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.core5.pool;
28  
29  import java.util.Collections;
30  import java.util.Random;
31  import java.util.concurrent.CancellationException;
32  import java.util.concurrent.CountDownLatch;
33  import java.util.concurrent.ExecutionException;
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.atomic.AtomicInteger;
39  import java.util.concurrent.atomic.AtomicReference;
40  
41  import org.apache.hc.core5.http.HttpConnection;
42  import org.apache.hc.core5.io.CloseMode;
43  import org.apache.hc.core5.util.DeadlineTimeoutException;
44  import org.apache.hc.core5.util.TimeValue;
45  import org.apache.hc.core5.util.Timeout;
46  import org.junit.jupiter.api.Assertions;
47  import org.junit.jupiter.api.Test;
48  import org.mockito.ArgumentMatchers;
49  import org.mockito.Mockito;
50  
51  public class TestStrictConnPool {
52  
53      @Test
54      public void testEmptyPool() throws Exception {
55          try (final StrictConnPool<String, HttpConnection> pool = new StrictConnPool<>(2, 10)) {
56              final PoolStats totals = pool.getTotalStats();
57              Assertions.assertEquals(0, totals.getAvailable());
58              Assertions.assertEquals(0, totals.getLeased());
59              Assertions.assertEquals(0, totals.getPending());
60              Assertions.assertEquals(10, totals.getMax());
61              Assertions.assertEquals(Collections.emptySet(), pool.getRoutes());
62              final PoolStats stats = pool.getStats("somehost");
63              Assertions.assertEquals(0, stats.getAvailable());
64              Assertions.assertEquals(0, stats.getLeased());
65              Assertions.assertEquals(0, stats.getPending());
66              Assertions.assertEquals(2, stats.getMax());
67              Assertions.assertEquals("[leased: 0][available: 0][pending: 0]", pool.toString());
68          }
69      }
70  
71      @Test
72      public void testInvalidConstruction() throws Exception {
73          Assertions.assertThrows(IllegalArgumentException.class, () ->
74                  new StrictConnPool<String, HttpConnection>(-1, 1));
75          Assertions.assertThrows(IllegalArgumentException.class, () ->
76                  new StrictConnPool<String, HttpConnection>(1, -1));
77      }
78  
79      @Test
80      public void testLeaseRelease() throws Exception {
81          final HttpConnection conn1 = Mockito.mock(HttpConnection.class);
82          final HttpConnection conn2 = Mockito.mock(HttpConnection.class);
83          final HttpConnection conn3 = Mockito.mock(HttpConnection.class);
84  
85          try (final StrictConnPool<String, HttpConnection> pool = new StrictConnPool<>(2, 10)) {
86              final Future<PoolEntry<String, HttpConnection>> future1 = pool.lease("somehost", null);
87              final Future<PoolEntry<String, HttpConnection>> future2 = pool.lease("somehost", null);
88              final Future<PoolEntry<String, HttpConnection>> future3 = pool.lease("otherhost", null);
89  
90              final PoolEntry<String, HttpConnection> entry1 = future1.get();
91              Assertions.assertNotNull(entry1);
92              entry1.assignConnection(conn1);
93              final PoolEntry<String, HttpConnection> entry2 = future2.get();
94              Assertions.assertNotNull(entry2);
95              entry2.assignConnection(conn2);
96              final PoolEntry<String, HttpConnection> entry3 = future3.get();
97              Assertions.assertNotNull(entry3);
98              entry3.assignConnection(conn3);
99  
100             pool.release(entry1, true);
101             pool.release(entry2, true);
102             pool.release(entry3, false);
103             Mockito.verify(conn1, Mockito.never()).close(ArgumentMatchers.any());
104             Mockito.verify(conn2, Mockito.never()).close(ArgumentMatchers.any());
105             Mockito.verify(conn3, Mockito.times(1)).close(CloseMode.GRACEFUL);
106 
107             final PoolStats totals = pool.getTotalStats();
108             Assertions.assertEquals(2, totals.getAvailable());
109             Assertions.assertEquals(0, totals.getLeased());
110             Assertions.assertEquals(0, totals.getPending());
111         }
112     }
113 
114     @Test
115     public void testLeaseReleaseMultiThreaded() throws Exception {
116         try (final StrictConnPool<String, HttpConnection> pool = new StrictConnPool<>(2, 10)) {
117 
118             final int c = 10;
119             final CountDownLatch latch = new CountDownLatch(c);
120             final AtomicInteger n = new AtomicInteger(c + 100);
121             final AtomicReference<AssertionError> exRef = new AtomicReference<>();
122 
123             final ExecutorService executorService = Executors.newFixedThreadPool(c);
124             try {
125                 final Random rnd = new Random();
126                 for (int i = 0; i < c; i++) {
127                     executorService.execute(() -> {
128                         try {
129                             while (n.decrementAndGet() > 0) {
130                                 try {
131                                     final Future<PoolEntry<String, HttpConnection>> future = pool.lease("somehost", null);
132                                     final PoolEntry<String, HttpConnection> poolEntry = future.get(1, TimeUnit.MINUTES);
133                                     Thread.sleep(rnd.nextInt(1));
134                                     pool.release(poolEntry, false);
135                                 } catch (final Exception ex) {
136                                     Assertions.fail(ex.getMessage(), ex);
137                                 }
138                             }
139                         } catch (final AssertionError ex) {
140                             exRef.compareAndSet(null, ex);
141                         } finally {
142                             latch.countDown();
143                         }
144                     });
145                 }
146 
147                 Assertions.assertTrue(latch.await(5, TimeUnit.MINUTES));
148             } finally {
149                 executorService.shutdownNow();
150             }
151 
152             final AssertionError assertionError = exRef.get();
153             if (assertionError != null) {
154                 throw assertionError;
155             }
156         }
157     }
158 
159     @Test
160     public void testLeaseInvalid() throws Exception {
161         try (final StrictConnPool<String, HttpConnection> pool = new StrictConnPool<>(2, 10)) {
162             Assertions.assertThrows(NullPointerException.class, () ->
163                     pool.lease(null, null, Timeout.ZERO_MILLISECONDS, null));
164             Assertions.assertThrows(NullPointerException.class, () ->
165                     pool.lease("somehost", null, null, null));
166     }
167     }
168 
169     @Test
170     public void testReleaseUnknownEntry() throws Exception {
171         try (final StrictConnPool<String, HttpConnection> pool = new StrictConnPool<>(2, 2)) {
172                 Assertions.assertThrows(IllegalStateException.class, () ->
173                     pool.release(new PoolEntry<>("somehost"), true));
174         }
175     }
176 
177     @Test
178     public void testMaxLimits() throws Exception {
179         final HttpConnection conn1 = Mockito.mock(HttpConnection.class);
180         final HttpConnection conn2 = Mockito.mock(HttpConnection.class);
181         final HttpConnection conn3 = Mockito.mock(HttpConnection.class);
182 
183         try (final StrictConnPool<String, HttpConnection> pool = new StrictConnPool<>(2, 10)) {
184             pool.setMaxPerRoute("somehost", 2);
185             pool.setMaxPerRoute("otherhost", 1);
186             pool.setMaxTotal(3);
187 
188             final Future<PoolEntry<String, HttpConnection>> future1 = pool.lease("somehost", null);
189             final Future<PoolEntry<String, HttpConnection>> future2 = pool.lease("somehost", null);
190             final Future<PoolEntry<String, HttpConnection>> future3 = pool.lease("otherhost", null);
191 
192             final PoolEntry<String, HttpConnection> entry1 = future1.get();
193             Assertions.assertNotNull(entry1);
194             entry1.assignConnection(conn1);
195             final PoolEntry<String, HttpConnection> entry2 = future2.get();
196             Assertions.assertNotNull(entry2);
197             entry2.assignConnection(conn2);
198             final PoolEntry<String, HttpConnection> entry3 = future3.get();
199             Assertions.assertNotNull(entry3);
200             entry3.assignConnection(conn3);
201 
202             pool.release(entry1, true);
203             pool.release(entry2, true);
204             pool.release(entry3, true);
205 
206             final PoolStats totals = pool.getTotalStats();
207             Assertions.assertEquals(3, totals.getAvailable());
208             Assertions.assertEquals(0, totals.getLeased());
209             Assertions.assertEquals(0, totals.getPending());
210 
211             final Future<PoolEntry<String, HttpConnection>> future4 = pool.lease("somehost", null);
212             final Future<PoolEntry<String, HttpConnection>> future5 = pool.lease("somehost", null);
213             final Future<PoolEntry<String, HttpConnection>> future6 = pool.lease("otherhost", null);
214             final Future<PoolEntry<String, HttpConnection>> future7 = pool.lease("somehost", null);
215             final Future<PoolEntry<String, HttpConnection>> future8 = pool.lease("somehost", null);
216             final Future<PoolEntry<String, HttpConnection>> future9 = pool.lease("otherhost", null);
217 
218             Assertions.assertTrue(future4.isDone());
219             final PoolEntry<String, HttpConnection> entry4 = future4.get();
220             Assertions.assertNotNull(entry4);
221             Assertions.assertSame(conn2, entry4.getConnection());
222 
223             Assertions.assertTrue(future5.isDone());
224             final PoolEntry<String, HttpConnection> entry5 = future5.get();
225             Assertions.assertNotNull(entry5);
226             Assertions.assertSame(conn1, entry5.getConnection());
227 
228             Assertions.assertTrue(future6.isDone());
229             final PoolEntry<String, HttpConnection> entry6 = future6.get();
230             Assertions.assertNotNull(entry6);
231             Assertions.assertSame(conn3, entry6.getConnection());
232 
233             Assertions.assertFalse(future7.isDone());
234             Assertions.assertFalse(future8.isDone());
235             Assertions.assertFalse(future9.isDone());
236 
237             pool.release(entry4, true);
238             pool.release(entry5, false);
239             pool.release(entry6, true);
240 
241             Assertions.assertTrue(future7.isDone());
242             final PoolEntry<String, HttpConnection> entry7 = future7.get();
243             Assertions.assertNotNull(entry7);
244             Assertions.assertSame(conn2, entry7.getConnection());
245 
246             Assertions.assertTrue(future8.isDone());
247             final PoolEntry<String, HttpConnection> entry8 = future8.get();
248             Assertions.assertNotNull(entry8);
249             Assertions.assertNull(entry8.getConnection());
250 
251             Assertions.assertTrue(future9.isDone());
252             final PoolEntry<String, HttpConnection> entry9 = future9.get();
253             Assertions.assertNotNull(entry9);
254             Assertions.assertSame(conn3, entry9.getConnection());
255         }
256     }
257 
258     @Test
259     public void testConnectionRedistributionOnTotalMaxLimit() throws Exception {
260         final HttpConnection conn1 = Mockito.mock(HttpConnection.class);
261         final HttpConnection conn2 = Mockito.mock(HttpConnection.class);
262         final HttpConnection conn3 = Mockito.mock(HttpConnection.class);
263         final HttpConnection conn4 = Mockito.mock(HttpConnection.class);
264         final HttpConnection conn5 = Mockito.mock(HttpConnection.class);
265 
266         try (final StrictConnPool<String, HttpConnection> pool = new StrictConnPool<>(2, 10)) {
267             pool.setMaxPerRoute("somehost", 2);
268             pool.setMaxPerRoute("otherhost", 2);
269             pool.setMaxTotal(2);
270 
271             final Future<PoolEntry<String, HttpConnection>> future1 = pool.lease("somehost", null);
272             final Future<PoolEntry<String, HttpConnection>> future2 = pool.lease("somehost", null);
273             final Future<PoolEntry<String, HttpConnection>> future3 = pool.lease("otherhost", null);
274             final Future<PoolEntry<String, HttpConnection>> future4 = pool.lease("otherhost", null);
275 
276             Assertions.assertTrue(future1.isDone());
277             final PoolEntry<String, HttpConnection> entry1 = future1.get();
278             Assertions.assertNotNull(entry1);
279             Assertions.assertFalse(entry1.hasConnection());
280             entry1.assignConnection(conn1);
281             Assertions.assertTrue(future2.isDone());
282             final PoolEntry<String, HttpConnection> entry2 = future2.get();
283             Assertions.assertNotNull(entry2);
284             Assertions.assertFalse(entry2.hasConnection());
285             entry2.assignConnection(conn2);
286 
287             Assertions.assertFalse(future3.isDone());
288             Assertions.assertFalse(future4.isDone());
289 
290             PoolStats totals = pool.getTotalStats();
291             Assertions.assertEquals(0, totals.getAvailable());
292             Assertions.assertEquals(2, totals.getLeased());
293             Assertions.assertEquals(2, totals.getPending());
294 
295             pool.release(entry1, true);
296             pool.release(entry2, true);
297 
298             Assertions.assertTrue(future3.isDone());
299             final PoolEntry<String, HttpConnection> entry3 = future3.get();
300             Assertions.assertNotNull(entry3);
301             Assertions.assertFalse(entry3.hasConnection());
302             entry3.assignConnection(conn3);
303             Assertions.assertTrue(future4.isDone());
304             final PoolEntry<String, HttpConnection> entry4 = future4.get();
305             Assertions.assertNotNull(entry4);
306             Assertions.assertFalse(entry4.hasConnection());
307             entry4.assignConnection(conn4);
308 
309             totals = pool.getTotalStats();
310             Assertions.assertEquals(0, totals.getAvailable());
311             Assertions.assertEquals(2, totals.getLeased());
312             Assertions.assertEquals(0, totals.getPending());
313 
314             final Future<PoolEntry<String, HttpConnection>> future5 = pool.lease("somehost", null);
315             final Future<PoolEntry<String, HttpConnection>> future6 = pool.lease("otherhost", null);
316 
317             pool.release(entry3, true);
318             pool.release(entry4, true);
319 
320             Assertions.assertTrue(future5.isDone());
321             final PoolEntry<String, HttpConnection> entry5 = future5.get();
322             Assertions.assertNotNull(entry5);
323             Assertions.assertFalse(entry5.hasConnection());
324             entry5.assignConnection(conn5);
325             Assertions.assertTrue(future6.isDone());
326             final PoolEntry<String, HttpConnection> entry6 = future6.get();
327             Assertions.assertNotNull(entry6);
328             Assertions.assertTrue(entry6.hasConnection());
329             Assertions.assertSame(conn4, entry6.getConnection());
330 
331             totals = pool.getTotalStats();
332             Assertions.assertEquals(0, totals.getAvailable());
333             Assertions.assertEquals(2, totals.getLeased());
334             Assertions.assertEquals(0, totals.getPending());
335 
336             pool.release(entry5, true);
337             pool.release(entry6, true);
338 
339             totals = pool.getTotalStats();
340             Assertions.assertEquals(2, totals.getAvailable());
341             Assertions.assertEquals(0, totals.getLeased());
342             Assertions.assertEquals(0, totals.getPending());}
343     }
344 
345     @Test
346     public void testStatefulConnectionRedistributionOnPerRouteMaxLimit() throws Exception {
347         final HttpConnection conn1 = Mockito.mock(HttpConnection.class);
348         final HttpConnection conn2 = Mockito.mock(HttpConnection.class);
349 
350         try (final StrictConnPool<String, HttpConnection> pool = new StrictConnPool<>(2, 10)) {
351             pool.setMaxPerRoute("somehost", 2);
352             pool.setMaxTotal(2);
353 
354             final Future<PoolEntry<String, HttpConnection>> future1 = pool.lease("somehost", null);
355             final Future<PoolEntry<String, HttpConnection>> future2 = pool.lease("somehost", null);
356 
357             Assertions.assertTrue(future1.isDone());
358             final PoolEntry<String, HttpConnection> entry1 = future1.get();
359             entry1.assignConnection(conn1);
360             Assertions.assertNotNull(entry1);
361             Assertions.assertTrue(future2.isDone());
362             final PoolEntry<String, HttpConnection> entry2 = future2.get();
363             Assertions.assertNotNull(entry2);
364             entry2.assignConnection(conn2);
365 
366             PoolStats totals = pool.getTotalStats();
367             Assertions.assertEquals(0, totals.getAvailable());
368             Assertions.assertEquals(2, totals.getLeased());
369             Assertions.assertEquals(0, totals.getPending());
370 
371             entry1.updateState("some-stuff");
372             pool.release(entry1, true);
373             entry2.updateState("some-stuff");
374             pool.release(entry2, true);
375 
376             final Future<PoolEntry<String, HttpConnection>> future3 = pool.lease("somehost", "some-stuff");
377             final Future<PoolEntry<String, HttpConnection>> future4 = pool.lease("somehost", "some-stuff");
378 
379             Assertions.assertTrue(future1.isDone());
380             final PoolEntry<String, HttpConnection> entry3 = future3.get();
381             Assertions.assertNotNull(entry3);
382             Assertions.assertSame(conn2, entry3.getConnection());
383             Assertions.assertTrue(future4.isDone());
384             final PoolEntry<String, HttpConnection> entry4 = future4.get();
385             Assertions.assertNotNull(entry4);
386             Assertions.assertSame(conn1, entry4.getConnection());
387 
388             pool.release(entry3, true);
389             pool.release(entry4, true);
390 
391             totals = pool.getTotalStats();
392             Assertions.assertEquals(2, totals.getAvailable());
393             Assertions.assertEquals(0, totals.getLeased());
394             Assertions.assertEquals(0, totals.getPending());
395 
396             final Future<PoolEntry<String, HttpConnection>> future5 = pool.lease("somehost", "some-other-stuff");
397 
398             Assertions.assertTrue(future5.isDone());
399 
400             Mockito.verify(conn2).close(CloseMode.GRACEFUL);
401             Mockito.verify(conn1, Mockito.never()).close(ArgumentMatchers.any());
402 
403             totals = pool.getTotalStats();
404             Assertions.assertEquals(1, totals.getAvailable());
405             Assertions.assertEquals(1, totals.getLeased());
406         }
407     }
408 
409     @Test
410     public void testCreateNewIfExpired() throws Exception {
411         final HttpConnection conn1 = Mockito.mock(HttpConnection.class);
412 
413         try (final StrictConnPool<String, HttpConnection> pool = new StrictConnPool<>(2, 2)) {
414 
415             final Future<PoolEntry<String, HttpConnection>> future1 = pool.lease("somehost", null);
416 
417             Assertions.assertTrue(future1.isDone());
418             final PoolEntry<String, HttpConnection> entry1 = future1.get();
419             Assertions.assertNotNull(entry1);
420             entry1.assignConnection(conn1);
421 
422             entry1.updateExpiry(TimeValue.of(1, TimeUnit.MILLISECONDS));
423             pool.release(entry1, true);
424 
425             Thread.sleep(200L);
426 
427             final Future<PoolEntry<String, HttpConnection>> future2 = pool.lease("somehost", null);
428 
429             Assertions.assertTrue(future2.isDone());
430 
431             Mockito.verify(conn1).close(CloseMode.GRACEFUL);
432 
433             final PoolStats totals = pool.getTotalStats();
434             Assertions.assertEquals(0, totals.getAvailable());
435             Assertions.assertEquals(1, totals.getLeased());
436             Assertions.assertEquals(Collections.singleton("somehost"), pool.getRoutes());
437             final PoolStats stats = pool.getStats("somehost");
438             Assertions.assertEquals(0, stats.getAvailable());
439             Assertions.assertEquals(1, stats.getLeased());
440         }
441     }
442 
443     @Test
444     public void testCloseExpired() throws Exception {
445         final HttpConnection conn1 = Mockito.mock(HttpConnection.class);
446         final HttpConnection conn2 = Mockito.mock(HttpConnection.class);
447 
448         try (final StrictConnPool<String, HttpConnection> pool = new StrictConnPool<>(2, 2)) {
449 
450             final Future<PoolEntry<String, HttpConnection>> future1 = pool.lease("somehost", null);
451             final Future<PoolEntry<String, HttpConnection>> future2 = pool.lease("somehost", null);
452 
453             Assertions.assertTrue(future1.isDone());
454             final PoolEntry<String, HttpConnection> entry1 = future1.get();
455             Assertions.assertNotNull(entry1);
456             entry1.assignConnection(conn1);
457             Assertions.assertTrue(future2.isDone());
458             final PoolEntry<String, HttpConnection> entry2 = future2.get();
459             Assertions.assertNotNull(entry2);
460             entry2.assignConnection(conn2);
461 
462             entry1.updateExpiry(TimeValue.of(1, TimeUnit.MILLISECONDS));
463             pool.release(entry1, true);
464 
465             Thread.sleep(200);
466 
467             entry2.updateExpiry(TimeValue.of(1000, TimeUnit.SECONDS));
468             pool.release(entry2, true);
469 
470             pool.closeExpired();
471 
472             Mockito.verify(conn1).close(CloseMode.GRACEFUL);
473             Mockito.verify(conn2, Mockito.never()).close(ArgumentMatchers.any());
474 
475             final PoolStats totals = pool.getTotalStats();
476             Assertions.assertEquals(1, totals.getAvailable());
477             Assertions.assertEquals(0, totals.getLeased());
478             Assertions.assertEquals(0, totals.getPending());
479             final PoolStats stats = pool.getStats("somehost");
480             Assertions.assertEquals(1, stats.getAvailable());
481             Assertions.assertEquals(0, stats.getLeased());
482             Assertions.assertEquals(0, stats.getPending());
483         }
484     }
485 
486     @Test
487     public void testCloseIdle() throws Exception {
488         final HttpConnection conn1 = Mockito.mock(HttpConnection.class);
489         final HttpConnection conn2 = Mockito.mock(HttpConnection.class);
490 
491         try (final StrictConnPool<String, HttpConnection> pool = new StrictConnPool<>(2, 2)) {
492 
493             final Future<PoolEntry<String, HttpConnection>> future1 = pool.lease("somehost", null);
494             final Future<PoolEntry<String, HttpConnection>> future2 = pool.lease("somehost", null);
495 
496             Assertions.assertTrue(future1.isDone());
497             final PoolEntry<String, HttpConnection> entry1 = future1.get();
498             Assertions.assertNotNull(entry1);
499             entry1.assignConnection(conn1);
500             Assertions.assertTrue(future2.isDone());
501             final PoolEntry<String, HttpConnection> entry2 = future2.get();
502             Assertions.assertNotNull(entry2);
503             entry2.assignConnection(conn2);
504 
505             entry1.updateState(null);
506             pool.release(entry1, true);
507 
508             Thread.sleep(200L);
509 
510             entry2.updateState(null);
511             pool.release(entry2, true);
512 
513             pool.closeIdle(TimeValue.of(50, TimeUnit.MILLISECONDS));
514 
515             Mockito.verify(conn1).close(CloseMode.GRACEFUL);
516             Mockito.verify(conn2, Mockito.never()).close(ArgumentMatchers.any());
517 
518             PoolStats totals = pool.getTotalStats();
519             Assertions.assertEquals(1, totals.getAvailable());
520             Assertions.assertEquals(0, totals.getLeased());
521             Assertions.assertEquals(0, totals.getPending());
522             PoolStats stats = pool.getStats("somehost");
523             Assertions.assertEquals(1, stats.getAvailable());
524             Assertions.assertEquals(0, stats.getLeased());
525             Assertions.assertEquals(0, stats.getPending());
526 
527             pool.closeIdle(TimeValue.of(-1, TimeUnit.MILLISECONDS));
528 
529             Mockito.verify(conn2).close(CloseMode.GRACEFUL);
530 
531             totals = pool.getTotalStats();
532             Assertions.assertEquals(0, totals.getAvailable());
533             Assertions.assertEquals(0, totals.getLeased());
534             Assertions.assertEquals(0, totals.getPending());
535             stats = pool.getStats("somehost");
536             Assertions.assertEquals(0, stats.getAvailable());
537             Assertions.assertEquals(0, stats.getLeased());
538             Assertions.assertEquals(0, stats.getPending());
539         }
540     }
541 
542     @Test
543     public void testLeaseRequestTimeout() throws Exception {
544         final HttpConnection conn1 = Mockito.mock(HttpConnection.class);
545 
546         try (final StrictConnPool<String, HttpConnection> pool = new StrictConnPool<>(1, 1)) {
547 
548             final Future<PoolEntry<String, HttpConnection>> future1 = pool.lease("somehost", null, Timeout.ofMilliseconds(0), null);
549             final Future<PoolEntry<String, HttpConnection>> future2 = pool.lease("somehost", null, Timeout.ofMilliseconds(0), null);
550             final Future<PoolEntry<String, HttpConnection>> future3 = pool.lease("somehost", null, Timeout.ofMilliseconds(10), null);
551 
552             Assertions.assertTrue(future1.isDone());
553             final PoolEntry<String, HttpConnection> entry1 = future1.get();
554             Assertions.assertNotNull(entry1);
555             entry1.assignConnection(conn1);
556             Assertions.assertFalse(future2.isDone());
557             Assertions.assertFalse(future3.isDone());
558 
559             Thread.sleep(100);
560 
561             pool.validatePendingRequests();
562 
563             Assertions.assertFalse(future2.isDone());
564             Assertions.assertTrue(future3.isDone());
565         }
566     }
567 
568     private static class HoldInternalLockThread extends Thread {
569         private HoldInternalLockThread(final StrictConnPool<String, HttpConnection> pool, final CountDownLatch lockHeld) {
570             super(() -> {
571                 pool.lease("somehost", null); // lease a connection so we have something to enumLeased()
572                 pool.enumLeased(object -> {
573                     try {
574                         lockHeld.countDown();
575                         Thread.sleep(Long.MAX_VALUE);
576                     } catch (final InterruptedException ignored) {
577                     }
578                 });
579             });
580         }
581     }
582 
583     @Test
584     public void testLeaseRequestLockTimeout() throws Exception {
585         final StrictConnPool<String, HttpConnection> pool = new StrictConnPool<>(1, 1);
586         final CountDownLatch lockHeld = new CountDownLatch(1);
587         final Thread holdInternalLock = new HoldInternalLockThread(pool, lockHeld);
588 
589         holdInternalLock.start(); // Start a thread to grab the internal conn pool lock
590         lockHeld.await(); // Wait until we know the internal lock is held
591 
592         // Attempt to get a connection while lock is held
593         final Future<PoolEntry<String, HttpConnection>> future2 = pool.lease("somehost", null, Timeout.ofMilliseconds(10), null);
594 
595         final ExecutionException executionException = Assertions.assertThrows(ExecutionException.class, () ->
596                 future2.get());
597         Assertions.assertTrue(executionException.getCause() instanceof DeadlineTimeoutException);
598         holdInternalLock.interrupt(); // Cleanup
599     }
600 
601     @Test
602     public void testLeaseRequestInterrupted() throws Exception {
603         final StrictConnPool<String, HttpConnection> pool = new StrictConnPool<>(1, 1);
604         final CountDownLatch lockHeld = new CountDownLatch(1);
605         final Thread holdInternalLock = new HoldInternalLockThread(pool, lockHeld);
606 
607         holdInternalLock.start(); // Start a thread to grab the internal conn pool lock
608         lockHeld.await(); // Wait until we know the internal lock is held
609 
610         Thread.currentThread().interrupt();
611         // Attempt to get a connection while lock is held and thread is interrupted
612         final Future<PoolEntry<String, HttpConnection>> future2 = pool.lease("somehost", null, Timeout.ofMilliseconds(10), null);
613 
614         Assertions.assertTrue(Thread.interrupted());
615         Assertions.assertThrows(CancellationException.class, () -> future2.get());
616         holdInternalLock.interrupt(); // Cleanup
617     }
618 
619     @Test
620     public void testLeaseRequestCanceled() throws Exception {
621         try (final StrictConnPool<String, HttpConnection> pool = new StrictConnPool<>(1, 1)) {
622 
623             final Future<PoolEntry<String, HttpConnection>> future1 = pool.lease("somehost", null,
624                     Timeout.ofMilliseconds(0), null);
625 
626             Assertions.assertTrue(future1.isDone());
627             final PoolEntry<String, HttpConnection> entry1 = future1.get();
628             Assertions.assertNotNull(entry1);
629             entry1.assignConnection(Mockito.mock(HttpConnection.class));
630 
631             final Future<PoolEntry<String, HttpConnection>> future2 = pool.lease("somehost", null,
632                     Timeout.ofMilliseconds(0), null);
633             future2.cancel(true);
634 
635             pool.release(entry1, true);
636 
637             final PoolStats totals = pool.getTotalStats();
638             Assertions.assertEquals(1, totals.getAvailable());
639             Assertions.assertEquals(0, totals.getLeased());
640         }
641     }
642 
643     @Test
644     public void testGetStatsInvalid() throws Exception {
645         try (final StrictConnPool<String, HttpConnection> pool = new StrictConnPool<>(2, 2)) {
646             Assertions.assertThrows(NullPointerException.class, () -> pool.getStats(null));
647         }
648     }
649 
650     @Test
651     public void testSetMaxInvalid() throws Exception {
652         try (final StrictConnPool<String, HttpConnection> pool = new StrictConnPool<>(2, 2)) {
653             Assertions.assertThrows(IllegalArgumentException.class, () ->
654                     pool.setMaxTotal(-1));
655             Assertions.assertThrows(NullPointerException.class, () ->
656                     pool.setMaxPerRoute(null, 1));
657             Assertions.assertThrows(IllegalArgumentException.class, () ->
658                     pool.setDefaultMaxPerRoute(-1));
659     }
660     }
661 
662     @Test
663     public void testSetMaxPerRoute() throws Exception {
664         try (final StrictConnPool<String, HttpConnection> pool = new StrictConnPool<>(2, 2)) {
665             pool.setMaxPerRoute("somehost", 1);
666             Assertions.assertEquals(1, pool.getMaxPerRoute("somehost"));
667             pool.setMaxPerRoute("somehost", 0);
668             Assertions.assertEquals(0, pool.getMaxPerRoute("somehost"));
669             pool.setMaxPerRoute("somehost", -1);
670             Assertions.assertEquals(2, pool.getMaxPerRoute("somehost"));
671         }
672     }
673 
674     @Test
675     public void testShutdown() throws Exception {
676         final StrictConnPool<String, HttpConnection> pool = new StrictConnPool<>(2, 2);
677         pool.close(CloseMode.GRACEFUL);
678         Assertions.assertThrows(IllegalStateException.class, () -> pool.lease("somehost", null));
679         // Ignored if shut down
680         pool.release(new PoolEntry<>("somehost"), true);
681     }
682 
683 }