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.CountDownLatch;
32  import java.util.concurrent.ExecutorService;
33  import java.util.concurrent.Executors;
34  import java.util.concurrent.Future;
35  import java.util.concurrent.TimeUnit;
36  import java.util.concurrent.atomic.AtomicInteger;
37  import java.util.concurrent.atomic.AtomicReference;
38  
39  import org.apache.hc.core5.http.HttpConnection;
40  import org.apache.hc.core5.io.CloseMode;
41  import org.apache.hc.core5.util.TimeValue;
42  import org.apache.hc.core5.util.Timeout;
43  import org.junit.jupiter.api.Assertions;
44  import org.junit.jupiter.api.Test;
45  import org.mockito.ArgumentMatchers;
46  import org.mockito.Mockito;
47  
48  public class TestLaxConnPool {
49  
50      @Test
51      public void testEmptyPool() throws Exception {
52          try (final LaxConnPool<String, HttpConnection> pool = new LaxConnPool<>(2)) {
53              final PoolStats totals = pool.getTotalStats();
54              Assertions.assertEquals(0, totals.getAvailable());
55              Assertions.assertEquals(0, totals.getLeased());
56              Assertions.assertEquals(0, totals.getPending());
57              Assertions.assertEquals(0, totals.getMax());
58              Assertions.assertEquals(Collections.emptySet(), pool.getRoutes());
59              final PoolStats stats = pool.getStats("somehost");
60              Assertions.assertEquals(0, stats.getAvailable());
61              Assertions.assertEquals(0, stats.getLeased());
62              Assertions.assertEquals(0, stats.getPending());
63              Assertions.assertEquals(2, stats.getMax());
64              Assertions.assertEquals("[leased: 0][available: 0][pending: 0]", pool.toString());
65          }
66      }
67  
68      @Test
69      public void testInvalidConstruction() throws Exception {
70          Assertions.assertThrows(IllegalArgumentException.class, () ->
71                  new LaxConnPool<String, HttpConnection>(-1));
72      }
73  
74      @Test
75      public void testLeaseRelease() throws Exception {
76          final HttpConnection conn1 = Mockito.mock(HttpConnection.class);
77          final HttpConnection conn2 = Mockito.mock(HttpConnection.class);
78          final HttpConnection conn3 = Mockito.mock(HttpConnection.class);
79  
80          try (final LaxConnPool<String, HttpConnection> pool = new LaxConnPool<>(2)) {
81              final Future<PoolEntry<String, HttpConnection>> future1 = pool.lease("somehost", null);
82              final Future<PoolEntry<String, HttpConnection>> future2 = pool.lease("somehost", null);
83              final Future<PoolEntry<String, HttpConnection>> future3 = pool.lease("otherhost", null);
84  
85              final PoolEntry<String, HttpConnection> entry1 = future1.get();
86              Assertions.assertNotNull(entry1);
87              entry1.assignConnection(conn1);
88              final PoolEntry<String, HttpConnection> entry2 = future2.get();
89              Assertions.assertNotNull(entry2);
90              entry2.assignConnection(conn2);
91              final PoolEntry<String, HttpConnection> entry3 = future3.get();
92              Assertions.assertNotNull(entry3);
93              entry3.assignConnection(conn3);
94  
95              pool.release(entry1, true);
96              pool.release(entry2, true);
97              pool.release(entry3, false);
98              Mockito.verify(conn1, Mockito.never()).close(ArgumentMatchers.any());
99              Mockito.verify(conn2, Mockito.never()).close(ArgumentMatchers.any());
100             Mockito.verify(conn3, Mockito.times(1)).close(CloseMode.GRACEFUL);
101 
102             final PoolStats totals = pool.getTotalStats();
103             Assertions.assertEquals(2, totals.getAvailable());
104             Assertions.assertEquals(0, totals.getLeased());
105             Assertions.assertEquals(0, totals.getPending());
106         }
107     }
108 
109     @Test
110     public void testLeaseReleaseMultiThreaded() throws Exception {
111         try (final LaxConnPool<String, HttpConnection> pool = new LaxConnPool<>(2)) {
112 
113             final int c = 10;
114             final CountDownLatch latch = new CountDownLatch(c);
115             final AtomicInteger n = new AtomicInteger(c + 100);
116             final AtomicReference<AssertionError> exRef = new AtomicReference<>();
117 
118             final ExecutorService executorService = Executors.newFixedThreadPool(c);
119             try {
120                 final Random rnd = new Random();
121                 for (int i = 0; i < c; i++) {
122                     executorService.execute(() -> {
123                         try {
124                             while (n.decrementAndGet() > 0) {
125                                 try {
126                                     final Future<PoolEntry<String, HttpConnection>> future = pool.lease("somehost", null);
127                                     final PoolEntry<String, HttpConnection> poolEntry = future.get(1, TimeUnit.MINUTES);
128                                     Thread.sleep(rnd.nextInt(1));
129                                     pool.release(poolEntry, false);
130                                 } catch (final Exception ex) {
131                                     Assertions.fail(ex.getMessage(), ex);
132                                 }
133                             }
134                         } catch (final AssertionError ex) {
135                             exRef.compareAndSet(null, ex);
136                         } finally {
137                             latch.countDown();
138                         }
139                     });
140                 }
141 
142                 Assertions.assertTrue(latch.await(5, TimeUnit.MINUTES));
143             } finally {
144                 executorService.shutdownNow();
145             }
146 
147             final AssertionError assertionError = exRef.get();
148             if (assertionError != null) {
149                 throw assertionError;
150             }
151         }
152     }
153 
154     @Test
155     public void testLeaseInvalid() throws Exception {
156         try (final LaxConnPool<String, HttpConnection> pool = new LaxConnPool<>(2)) {
157             Assertions.assertThrows(NullPointerException.class,
158                     () -> pool.lease(null, null, Timeout.ZERO_MILLISECONDS, null));
159         }}
160 
161     @Test
162     public void testReleaseUnknownEntry() throws Exception {
163         try (final LaxConnPool<String, HttpConnection> pool = new LaxConnPool<>(2)) {
164             Assertions.assertThrows(IllegalStateException.class, () -> pool.release(new PoolEntry<>("somehost"), true));
165         }
166     }
167 
168     @Test
169     public void testMaxLimits() throws Exception {
170         final HttpConnection conn1 = Mockito.mock(HttpConnection.class);
171         final HttpConnection conn2 = Mockito.mock(HttpConnection.class);
172         final HttpConnection conn3 = Mockito.mock(HttpConnection.class);
173 
174         try (final LaxConnPool<String, HttpConnection> pool = new LaxConnPool<>(2)) {
175             pool.setMaxPerRoute("somehost", 2);
176             pool.setMaxPerRoute("otherhost", 1);
177 
178             final Future<PoolEntry<String, HttpConnection>> future1 = pool.lease("somehost", null);
179             final Future<PoolEntry<String, HttpConnection>> future2 = pool.lease("somehost", null);
180             final Future<PoolEntry<String, HttpConnection>> future3 = pool.lease("otherhost", null);
181 
182             final PoolEntry<String, HttpConnection> entry1 = future1.get();
183             Assertions.assertNotNull(entry1);
184             entry1.assignConnection(conn1);
185             final PoolEntry<String, HttpConnection> entry2 = future2.get();
186             Assertions.assertNotNull(entry2);
187             entry2.assignConnection(conn2);
188             final PoolEntry<String, HttpConnection> entry3 = future3.get();
189             Assertions.assertNotNull(entry3);
190             entry3.assignConnection(conn3);
191 
192             pool.release(entry1, true);
193             pool.release(entry2, true);
194             pool.release(entry3, true);
195 
196             final PoolStats totals = pool.getTotalStats();
197             Assertions.assertEquals(3, totals.getAvailable());
198             Assertions.assertEquals(0, totals.getLeased());
199             Assertions.assertEquals(0, totals.getPending());
200 
201             final Future<PoolEntry<String, HttpConnection>> future4 = pool.lease("somehost", null);
202             final Future<PoolEntry<String, HttpConnection>> future5 = pool.lease("somehost", null);
203             final Future<PoolEntry<String, HttpConnection>> future6 = pool.lease("otherhost", null);
204             final Future<PoolEntry<String, HttpConnection>> future7 = pool.lease("somehost", null);
205             final Future<PoolEntry<String, HttpConnection>> future8 = pool.lease("somehost", null);
206             final Future<PoolEntry<String, HttpConnection>> future9 = pool.lease("otherhost", null);
207 
208             Assertions.assertTrue(future4.isDone());
209             final PoolEntry<String, HttpConnection> entry4 = future4.get();
210             Assertions.assertNotNull(entry4);
211             Assertions.assertSame(conn2, entry4.getConnection());
212 
213             Assertions.assertTrue(future5.isDone());
214             final PoolEntry<String, HttpConnection> entry5 = future5.get();
215             Assertions.assertNotNull(entry5);
216             Assertions.assertSame(conn1, entry5.getConnection());
217 
218             Assertions.assertTrue(future6.isDone());
219             final PoolEntry<String, HttpConnection> entry6 = future6.get();
220             Assertions.assertNotNull(entry6);
221             Assertions.assertSame(conn3, entry6.getConnection());
222 
223             Assertions.assertFalse(future7.isDone());
224             Assertions.assertFalse(future8.isDone());
225             Assertions.assertFalse(future9.isDone());
226 
227             pool.release(entry4, true);
228             pool.release(entry5, false);
229             pool.release(entry6, true);
230 
231             Assertions.assertTrue(future7.isDone());
232             final PoolEntry<String, HttpConnection> entry7 = future7.get();
233             Assertions.assertNotNull(entry7);
234             Assertions.assertSame(conn2, entry7.getConnection());
235 
236             Assertions.assertTrue(future8.isDone());
237             final PoolEntry<String, HttpConnection> entry8 = future8.get();
238             Assertions.assertNotNull(entry8);
239             Assertions.assertNull(entry8.getConnection());
240 
241             Assertions.assertTrue(future9.isDone());
242             final PoolEntry<String, HttpConnection> entry9 = future9.get();
243             Assertions.assertNotNull(entry9);
244             Assertions.assertSame(conn3, entry9.getConnection());
245         }
246     }
247 
248     @Test
249     public void testCreateNewIfExpired() throws Exception {
250         final HttpConnection conn1 = Mockito.mock(HttpConnection.class);
251 
252         try (final LaxConnPool<String, HttpConnection> pool = new LaxConnPool<>(2)) {
253 
254             final Future<PoolEntry<String, HttpConnection>> future1 = pool.lease("somehost", null);
255 
256             Assertions.assertTrue(future1.isDone());
257             final PoolEntry<String, HttpConnection> entry1 = future1.get();
258             Assertions.assertNotNull(entry1);
259             entry1.assignConnection(conn1);
260 
261             entry1.updateExpiry(TimeValue.of(1, TimeUnit.MILLISECONDS));
262             pool.release(entry1, true);
263 
264             Thread.sleep(200L);
265 
266             final Future<PoolEntry<String, HttpConnection>> future2 = pool.lease("somehost", null);
267 
268             Assertions.assertTrue(future2.isDone());
269 
270             Mockito.verify(conn1).close(CloseMode.GRACEFUL);
271 
272             final PoolStats totals = pool.getTotalStats();
273             Assertions.assertEquals(0, totals.getAvailable());
274             Assertions.assertEquals(1, totals.getLeased());
275             Assertions.assertEquals(Collections.singleton("somehost"), pool.getRoutes());
276             final PoolStats stats = pool.getStats("somehost");
277             Assertions.assertEquals(0, stats.getAvailable());
278             Assertions.assertEquals(1, stats.getLeased());
279         }
280     }
281 
282     @Test
283     public void testCloseExpired() throws Exception {
284         final HttpConnection conn1 = Mockito.mock(HttpConnection.class);
285         final HttpConnection conn2 = Mockito.mock(HttpConnection.class);
286 
287         try (final LaxConnPool<String, HttpConnection> pool = new LaxConnPool<>(2)) {
288 
289             final Future<PoolEntry<String, HttpConnection>> future1 = pool.lease("somehost", null);
290             final Future<PoolEntry<String, HttpConnection>> future2 = pool.lease("somehost", null);
291 
292             Assertions.assertTrue(future1.isDone());
293             final PoolEntry<String, HttpConnection> entry1 = future1.get();
294             Assertions.assertNotNull(entry1);
295             entry1.assignConnection(conn1);
296             Assertions.assertTrue(future2.isDone());
297             final PoolEntry<String, HttpConnection> entry2 = future2.get();
298             Assertions.assertNotNull(entry2);
299             entry2.assignConnection(conn2);
300 
301             entry1.updateExpiry(TimeValue.of(1, TimeUnit.MILLISECONDS));
302             pool.release(entry1, true);
303 
304             Thread.sleep(200);
305 
306             entry2.updateExpiry(TimeValue.of(1000, TimeUnit.SECONDS));
307             pool.release(entry2, true);
308 
309             pool.closeExpired();
310 
311             Mockito.verify(conn1).close(CloseMode.GRACEFUL);
312             Mockito.verify(conn2, Mockito.never()).close(ArgumentMatchers.any());
313 
314             final PoolStats totals = pool.getTotalStats();
315             Assertions.assertEquals(1, totals.getAvailable());
316             Assertions.assertEquals(0, totals.getLeased());
317             Assertions.assertEquals(0, totals.getPending());
318             final PoolStats stats = pool.getStats("somehost");
319             Assertions.assertEquals(1, stats.getAvailable());
320             Assertions.assertEquals(0, stats.getLeased());
321             Assertions.assertEquals(0, stats.getPending());
322         }
323     }
324 
325     @Test
326     public void testCloseIdle() throws Exception {
327         final HttpConnection conn1 = Mockito.mock(HttpConnection.class);
328         final HttpConnection conn2 = Mockito.mock(HttpConnection.class);
329 
330         try (final LaxConnPool<String, HttpConnection> pool = new LaxConnPool<>(2)) {
331 
332             final Future<PoolEntry<String, HttpConnection>> future1 = pool.lease("somehost", null);
333             final Future<PoolEntry<String, HttpConnection>> future2 = pool.lease("somehost", null);
334 
335             Assertions.assertTrue(future1.isDone());
336             final PoolEntry<String, HttpConnection> entry1 = future1.get();
337             Assertions.assertNotNull(entry1);
338             entry1.assignConnection(conn1);
339             Assertions.assertTrue(future2.isDone());
340             final PoolEntry<String, HttpConnection> entry2 = future2.get();
341             Assertions.assertNotNull(entry2);
342             entry2.assignConnection(conn2);
343 
344             entry1.updateState(null);
345             pool.release(entry1, true);
346 
347             Thread.sleep(200L);
348 
349             entry2.updateState(null);
350             pool.release(entry2, true);
351 
352             pool.closeIdle(TimeValue.of(50, TimeUnit.MILLISECONDS));
353 
354             Mockito.verify(conn1).close(CloseMode.GRACEFUL);
355             Mockito.verify(conn2, Mockito.never()).close(ArgumentMatchers.any());
356 
357             PoolStats totals = pool.getTotalStats();
358             Assertions.assertEquals(1, totals.getAvailable());
359             Assertions.assertEquals(0, totals.getLeased());
360             Assertions.assertEquals(0, totals.getPending());
361             PoolStats stats = pool.getStats("somehost");
362             Assertions.assertEquals(1, stats.getAvailable());
363             Assertions.assertEquals(0, stats.getLeased());
364             Assertions.assertEquals(0, stats.getPending());
365 
366             pool.closeIdle(TimeValue.of(-1, TimeUnit.MILLISECONDS));
367 
368             Mockito.verify(conn2).close(CloseMode.GRACEFUL);
369 
370             totals = pool.getTotalStats();
371             Assertions.assertEquals(0, totals.getAvailable());
372             Assertions.assertEquals(0, totals.getLeased());
373             Assertions.assertEquals(0, totals.getPending());
374             stats = pool.getStats("somehost");
375             Assertions.assertEquals(0, stats.getAvailable());
376             Assertions.assertEquals(0, stats.getLeased());
377             Assertions.assertEquals(0, stats.getPending());
378 
379             Assertions.assertFalse(pool.isShutdown());
380         }
381     }
382 
383     @Test
384     public void testLeaseRequestTimeout() throws Exception {
385         final HttpConnection conn1 = Mockito.mock(HttpConnection.class);
386 
387         try (final LaxConnPool<String, HttpConnection> pool = new LaxConnPool<>(1)) {
388 
389             final Future<PoolEntry<String, HttpConnection>> future1 = pool.lease("somehost", null, Timeout.ofMilliseconds(0), null);
390             final Future<PoolEntry<String, HttpConnection>> future2 = pool.lease("somehost", null, Timeout.ofMilliseconds(0), null);
391             final Future<PoolEntry<String, HttpConnection>> future3 = pool.lease("somehost", null, Timeout.ofMilliseconds(10), null);
392 
393             Assertions.assertTrue(future1.isDone());
394             final PoolEntry<String, HttpConnection> entry1 = future1.get();
395             Assertions.assertNotNull(entry1);
396             entry1.assignConnection(conn1);
397             Assertions.assertFalse(future2.isDone());
398             Assertions.assertFalse(future3.isDone());
399 
400             Thread.sleep(100);
401 
402             pool.validatePendingRequests();
403 
404             Assertions.assertFalse(future2.isDone());
405             Assertions.assertTrue(future3.isDone());
406         }
407     }
408 
409     @Test
410     public void testLeaseRequestCanceled() throws Exception {
411         try (final LaxConnPool<String, HttpConnection> pool = new LaxConnPool<>(1)) {
412 
413             final Future<PoolEntry<String, HttpConnection>> future1 = pool.lease("somehost", null,
414                     Timeout.ofMilliseconds(0), null);
415 
416             Assertions.assertTrue(future1.isDone());
417             final PoolEntry<String, HttpConnection> entry1 = future1.get();
418             Assertions.assertNotNull(entry1);
419             entry1.assignConnection(Mockito.mock(HttpConnection.class));
420 
421             final Future<PoolEntry<String, HttpConnection>> future2 = pool.lease("somehost", null,
422                     Timeout.ofMilliseconds(0), null);
423             future2.cancel(true);
424 
425             pool.release(entry1, true);
426 
427             final PoolStats totals = pool.getTotalStats();
428             Assertions.assertEquals(1, totals.getAvailable());
429             Assertions.assertEquals(0, totals.getLeased());
430         }
431     }
432 
433     @Test
434     public void testGetStatsInvalid() throws Exception {
435         try (final LaxConnPool<String, HttpConnection> pool = new LaxConnPool<>(2)) {
436             Assertions.assertThrows(NullPointerException.class, () -> pool.getStats(null));
437         }
438     }
439 
440     @Test
441     public void testSetMaxInvalid() throws Exception {
442         try (final LaxConnPool<String, HttpConnection> pool = new LaxConnPool<>(2)) {
443             Assertions.assertThrows(NullPointerException.class, () -> pool.setMaxPerRoute(null, 1));
444             Assertions.assertThrows(IllegalArgumentException.class, () -> pool.setDefaultMaxPerRoute(-1));
445         }
446     }
447 
448     @Test
449     public void testShutdown() throws Exception {
450         final LaxConnPool<String, HttpConnection> pool = new LaxConnPool<>(2);
451         pool.close(CloseMode.GRACEFUL);
452         Assertions.assertThrows(IllegalStateException.class, () -> pool.lease("somehost", null));
453         // Ignored if shut down
454         pool.release(new PoolEntry<>("somehost"), true);
455     }
456 
457     @Test
458     public void testClose() {
459         final LaxConnPool<String, HttpConnection> pool = new LaxConnPool<>(2);
460         pool.setMaxPerRoute("someRoute", 2);
461         pool.close();
462         Assertions.assertThrows(IllegalStateException.class, () -> pool.lease("someHost", null));
463         // Ignored if shut down
464         pool.release(new PoolEntry<>("someHost"), true);
465 
466     }
467 
468     @Test
469     public void testGetMaxPerRoute() {
470         final String route = "someRoute";
471         final int max = 2;
472         try (final LaxConnPool<String, HttpConnection> pool = new LaxConnPool<>(2)) {
473             pool.setMaxPerRoute(route, max);
474             Assertions.assertEquals(max, pool.getMaxPerRoute(route));
475         }
476     }
477 }