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.http.impl.client.integration;
29  
30  import java.io.IOException;
31  import java.net.InetSocketAddress;
32  import java.net.Socket;
33  import java.net.SocketException;
34  import java.util.concurrent.CountDownLatch;
35  import java.util.concurrent.ExecutionException;
36  import java.util.concurrent.TimeUnit;
37  import java.util.concurrent.atomic.AtomicReference;
38  
39  import org.apache.http.HttpClientConnection;
40  import org.apache.http.HttpHost;
41  import org.apache.http.HttpRequest;
42  import org.apache.http.HttpRequestInterceptor;
43  import org.apache.http.HttpResponse;
44  import org.apache.http.HttpStatus;
45  import org.apache.http.HttpVersion;
46  import org.apache.http.config.Registry;
47  import org.apache.http.config.RegistryBuilder;
48  import org.apache.http.conn.ConnectTimeoutException;
49  import org.apache.http.conn.ConnectionPoolTimeoutException;
50  import org.apache.http.conn.ConnectionRequest;
51  import org.apache.http.conn.HttpClientConnectionManager;
52  import org.apache.http.conn.routing.HttpRoute;
53  import org.apache.http.conn.socket.ConnectionSocketFactory;
54  import org.apache.http.conn.socket.PlainConnectionSocketFactory;
55  import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
56  import org.apache.http.localserver.LocalServerTestBase;
57  import org.apache.http.message.BasicHttpRequest;
58  import org.apache.http.protocol.BasicHttpContext;
59  import org.apache.http.protocol.HttpContext;
60  import org.apache.http.protocol.HttpCoreContext;
61  import org.apache.http.protocol.HttpProcessor;
62  import org.apache.http.protocol.HttpRequestExecutor;
63  import org.apache.http.protocol.ImmutableHttpProcessor;
64  import org.apache.http.protocol.RequestConnControl;
65  import org.apache.http.protocol.RequestContent;
66  import org.apache.http.util.EntityUtils;
67  import org.junit.Assert;
68  import org.junit.Test;
69  
70  /**
71   * Tests for <code>PoolingHttpClientConnectionManager</code> that do require a server
72   * to communicate with.
73   */
74  public class TestConnectionManagement extends LocalServerTestBase {
75  
76      private static HttpClientConnection getConnection(
77              final HttpClientConnectionManager mgr,
78              final HttpRoute route,
79              final long timeout,
80              final TimeUnit unit) throws ConnectionPoolTimeoutException, ExecutionException, InterruptedException {
81          final ConnectionRequest connRequest = mgr.requestConnection(route, null);
82          return connRequest.get(timeout, unit);
83      }
84  
85      private static HttpClientConnection getConnection(
86              final HttpClientConnectionManager mgr,
87              final HttpRoute route) throws ConnectionPoolTimeoutException, ExecutionException, InterruptedException {
88          final ConnectionRequest connRequest = mgr.requestConnection(route, null);
89          return connRequest.get(0, TimeUnit.MILLISECONDS);
90      }
91  
92      /**
93       * Tests releasing and re-using a connection after a response is read.
94       */
95      @Test
96      public void testReleaseConnection() throws Exception {
97  
98          this.connManager.setMaxTotal(1);
99  
100         final HttpHost target = start();
101         final HttpRoute route = new HttpRoute(target, null, false);
102         final int      rsplen = 8;
103         final String      uri = "/random/" + rsplen;
104 
105         final HttpRequest request = new BasicHttpRequest("GET", uri, HttpVersion.HTTP_1_1);
106         final HttpContext context = new BasicHttpContext();
107 
108         HttpClientConnection conn = getConnection(this.connManager, route);
109         this.connManager.connect(conn, route, 0, context);
110         this.connManager.routeComplete(conn, route, context);
111 
112         context.setAttribute(HttpCoreContext.HTTP_CONNECTION, conn);
113         context.setAttribute(HttpCoreContext.HTTP_TARGET_HOST, target);
114 
115         final HttpProcessor httpProcessor = new ImmutableHttpProcessor(
116                 new HttpRequestInterceptor[] { new RequestContent(), new RequestConnControl() });
117 
118         final HttpRequestExecutor exec = new HttpRequestExecutor();
119         exec.preProcess(request, httpProcessor, context);
120         HttpResponse response = exec.execute(request, conn, context);
121 
122         Assert.assertEquals("wrong status in first response",
123                      HttpStatus.SC_OK,
124                      response.getStatusLine().getStatusCode());
125         byte[] data = EntityUtils.toByteArray(response.getEntity());
126         Assert.assertEquals("wrong length of first response entity",
127                      rsplen, data.length);
128         // ignore data, but it must be read
129 
130         // check that there is no auto-release by default
131         try {
132             // this should fail quickly, connection has not been released
133             getConnection(this.connManager, route, 10L, TimeUnit.MILLISECONDS);
134             Assert.fail("ConnectionPoolTimeoutException should have been thrown");
135         } catch (final ConnectionPoolTimeoutException e) {
136             // expected
137         }
138 
139         conn.close();
140         this.connManager.releaseConnection(conn, null, -1, null);
141         conn = getConnection(this.connManager, route);
142         Assert.assertFalse("connection should have been closed", conn.isOpen());
143 
144         this.connManager.connect(conn, route, 0, context);
145         this.connManager.routeComplete(conn, route, context);
146 
147         // repeat the communication, no need to prepare the request again
148         context.setAttribute(HttpCoreContext.HTTP_CONNECTION, conn);
149         response = exec.execute(request, conn, context);
150 
151         Assert.assertEquals("wrong status in second response",
152                      HttpStatus.SC_OK,
153                      response.getStatusLine().getStatusCode());
154         data = EntityUtils.toByteArray(response.getEntity());
155         Assert.assertEquals("wrong length of second response entity",
156                      rsplen, data.length);
157         // ignore data, but it must be read
158 
159         // release connection after marking it for re-use
160         // expect the next connection obtained to be open
161         this.connManager.releaseConnection(conn, null, -1, null);
162         conn = getConnection(this.connManager, route);
163         Assert.assertTrue("connection should have been open", conn.isOpen());
164 
165         // repeat the communication, no need to prepare the request again
166         context.setAttribute(HttpCoreContext.HTTP_CONNECTION, conn);
167         response = exec.execute(request, conn, context);
168 
169         Assert.assertEquals("wrong status in third response",
170                      HttpStatus.SC_OK,
171                      response.getStatusLine().getStatusCode());
172         data = EntityUtils.toByteArray(response.getEntity());
173         Assert.assertEquals("wrong length of third response entity",
174                      rsplen, data.length);
175         // ignore data, but it must be read
176 
177         this.connManager.releaseConnection(conn, null, -1, null);
178         this.connManager.shutdown();
179     }
180 
181     /**
182      * Tests releasing with time limits.
183      */
184     @Test
185     public void testReleaseConnectionWithTimeLimits() throws Exception {
186 
187         this.connManager.setMaxTotal(1);
188 
189         final HttpHost target = start();
190         final HttpRoute route = new HttpRoute(target, null, false);
191         final int      rsplen = 8;
192         final String      uri = "/random/" + rsplen;
193 
194         final HttpRequest request = new BasicHttpRequest("GET", uri, HttpVersion.HTTP_1_1);
195         final HttpContext context = new BasicHttpContext();
196 
197         HttpClientConnection conn = getConnection(this.connManager, route);
198         this.connManager.connect(conn, route, 0, context);
199         this.connManager.routeComplete(conn, route, context);
200 
201         context.setAttribute(HttpCoreContext.HTTP_CONNECTION, conn);
202         context.setAttribute(HttpCoreContext.HTTP_TARGET_HOST, target);
203 
204         final HttpProcessor httpProcessor = new ImmutableHttpProcessor(
205                 new HttpRequestInterceptor[] { new RequestContent(), new RequestConnControl() });
206 
207         final HttpRequestExecutor exec = new HttpRequestExecutor();
208         exec.preProcess(request, httpProcessor, context);
209         HttpResponse response = exec.execute(request, conn, context);
210 
211         Assert.assertEquals("wrong status in first response",
212                      HttpStatus.SC_OK,
213                      response.getStatusLine().getStatusCode());
214         byte[] data = EntityUtils.toByteArray(response.getEntity());
215         Assert.assertEquals("wrong length of first response entity",
216                      rsplen, data.length);
217         // ignore data, but it must be read
218 
219         // check that there is no auto-release by default
220         try {
221             // this should fail quickly, connection has not been released
222             getConnection(this.connManager, route, 10L, TimeUnit.MILLISECONDS);
223             Assert.fail("ConnectionPoolTimeoutException should have been thrown");
224         } catch (final ConnectionPoolTimeoutException e) {
225             // expected
226         }
227 
228         conn.close();
229         this.connManager.releaseConnection(conn, null, 100, TimeUnit.MILLISECONDS);
230         conn = getConnection(this.connManager, route);
231         Assert.assertFalse("connection should have been closed", conn.isOpen());
232 
233         // repeat the communication, no need to prepare the request again
234         this.connManager.connect(conn, route, 0, context);
235         this.connManager.routeComplete(conn, route, context);
236 
237         context.setAttribute(HttpCoreContext.HTTP_CONNECTION, conn);
238         response = exec.execute(request, conn, context);
239 
240         Assert.assertEquals("wrong status in second response",
241                      HttpStatus.SC_OK,
242                      response.getStatusLine().getStatusCode());
243         data = EntityUtils.toByteArray(response.getEntity());
244         Assert.assertEquals("wrong length of second response entity",
245                      rsplen, data.length);
246         // ignore data, but it must be read
247 
248         this.connManager.releaseConnection(conn, null, 100, TimeUnit.MILLISECONDS);
249         conn = getConnection(this.connManager, route);
250         Assert.assertTrue("connection should have been open", conn.isOpen());
251 
252         // repeat the communication, no need to prepare the request again
253         context.setAttribute(HttpCoreContext.HTTP_CONNECTION, conn);
254         response = exec.execute(request, conn, context);
255 
256         Assert.assertEquals("wrong status in third response",
257                      HttpStatus.SC_OK,
258                      response.getStatusLine().getStatusCode());
259         data = EntityUtils.toByteArray(response.getEntity());
260         Assert.assertEquals("wrong length of third response entity",
261                      rsplen, data.length);
262         // ignore data, but it must be read
263 
264         this.connManager.releaseConnection(conn, null, 100, TimeUnit.MILLISECONDS);
265         Thread.sleep(150);
266         conn = getConnection(this.connManager, route);
267         Assert.assertTrue("connection should have been closed", !conn.isOpen());
268 
269         // repeat the communication, no need to prepare the request again
270         this.connManager.connect(conn, route, 0, context);
271         this.connManager.routeComplete(conn, route, context);
272 
273         context.setAttribute(HttpCoreContext.HTTP_CONNECTION, conn);
274         response = exec.execute(request, conn, context);
275 
276         Assert.assertEquals("wrong status in third response",
277                      HttpStatus.SC_OK,
278                      response.getStatusLine().getStatusCode());
279         data = EntityUtils.toByteArray(response.getEntity());
280         Assert.assertEquals("wrong length of fourth response entity",
281                      rsplen, data.length);
282         // ignore data, but it must be read
283 
284         this.connManager.shutdown();
285     }
286 
287     @Test
288     public void testCloseExpiredIdleConnections() throws Exception {
289 
290         this.connManager.setMaxTotal(1);
291 
292         final HttpHost target = start();
293         final HttpRoute route = new HttpRoute(target, null, false);
294         final HttpContext context = new BasicHttpContext();
295 
296         final HttpClientConnection conn = getConnection(this.connManager, route);
297         this.connManager.connect(conn, route, 0, context);
298         this.connManager.routeComplete(conn, route, context);
299 
300         Assert.assertEquals(1, this.connManager.getTotalStats().getLeased());
301         Assert.assertEquals(1, this.connManager.getStats(route).getLeased());
302 
303         this.connManager.releaseConnection(conn, null, 100, TimeUnit.MILLISECONDS);
304 
305         // Released, still active.
306         Assert.assertEquals(1, this.connManager.getTotalStats().getAvailable());
307         Assert.assertEquals(1, this.connManager.getStats(route).getAvailable());
308 
309         this.connManager.closeExpiredConnections();
310 
311         // Time has not expired yet.
312         Assert.assertEquals(1, this.connManager.getTotalStats().getAvailable());
313         Assert.assertEquals(1, this.connManager.getStats(route).getAvailable());
314 
315         Thread.sleep(150);
316 
317         this.connManager.closeExpiredConnections();
318 
319         // Time expired now, connections are destroyed.
320         Assert.assertEquals(0, this.connManager.getTotalStats().getAvailable());
321         Assert.assertEquals(0, this.connManager.getStats(route).getAvailable());
322 
323         this.connManager.shutdown();
324     }
325 
326     @Test
327     public void testCloseExpiredTTLConnections() throws Exception {
328 
329         this.connManager = new PoolingHttpClientConnectionManager(
330                 100, TimeUnit.MILLISECONDS);
331         this.clientBuilder.setConnectionManager(this.connManager);
332 
333         this.connManager.setMaxTotal(1);
334 
335         final HttpHost target = start();
336         final HttpRoute route = new HttpRoute(target, null, false);
337         final HttpContext context = new BasicHttpContext();
338 
339         final HttpClientConnection conn = getConnection(this.connManager, route);
340         this.connManager.connect(conn, route, 0, context);
341         this.connManager.routeComplete(conn, route, context);
342 
343         Assert.assertEquals(1, this.connManager.getTotalStats().getLeased());
344         Assert.assertEquals(1, this.connManager.getStats(route).getLeased());
345         // Release, let remain idle for forever
346         this.connManager.releaseConnection(conn, null, -1, TimeUnit.MILLISECONDS);
347 
348         // Released, still active.
349         Assert.assertEquals(1, this.connManager.getTotalStats().getAvailable());
350         Assert.assertEquals(1, this.connManager.getStats(route).getAvailable());
351 
352         this.connManager.closeExpiredConnections();
353 
354         // Time has not expired yet.
355         Assert.assertEquals(1, this.connManager.getTotalStats().getAvailable());
356         Assert.assertEquals(1, this.connManager.getStats(route).getAvailable());
357 
358         Thread.sleep(150);
359 
360         this.connManager.closeExpiredConnections();
361 
362         // TTL expired now, connections are destroyed.
363         Assert.assertEquals(0, this.connManager.getTotalStats().getAvailable());
364         Assert.assertEquals(0, this.connManager.getStats(route).getAvailable());
365 
366         this.connManager.shutdown();
367     }
368 
369     /**
370      * Tests releasing connection from #abort method called from the
371      * main execution thread while there is no blocking I/O operation.
372      */
373     @Test
374     public void testReleaseConnectionOnAbort() throws Exception {
375 
376         this.connManager.setMaxTotal(1);
377 
378         final HttpHost target = start();
379         final HttpRoute route = new HttpRoute(target, null, false);
380         final int      rsplen = 8;
381         final String      uri = "/random/" + rsplen;
382         final HttpContext context = new BasicHttpContext();
383 
384         final HttpRequest request =
385             new BasicHttpRequest("GET", uri, HttpVersion.HTTP_1_1);
386 
387         HttpClientConnection conn = getConnection(this.connManager, route);
388         this.connManager.connect(conn, route, 0, context);
389         this.connManager.routeComplete(conn, route, context);
390 
391         context.setAttribute(HttpCoreContext.HTTP_CONNECTION, conn);
392         context.setAttribute(HttpCoreContext.HTTP_TARGET_HOST, target);
393 
394         final HttpProcessor httpProcessor = new ImmutableHttpProcessor(
395                 new HttpRequestInterceptor[] { new RequestContent(), new RequestConnControl() });
396 
397         final HttpRequestExecutor exec = new HttpRequestExecutor();
398         exec.preProcess(request, httpProcessor, context);
399         final HttpResponse response = exec.execute(request, conn, context);
400 
401         Assert.assertEquals("wrong status in first response",
402                      HttpStatus.SC_OK,
403                      response.getStatusLine().getStatusCode());
404 
405         // check that there are no connections available
406         try {
407             // this should fail quickly, connection has not been released
408             getConnection(this.connManager, route, 100L, TimeUnit.MILLISECONDS);
409             Assert.fail("ConnectionPoolTimeoutException should have been thrown");
410         } catch (final ConnectionPoolTimeoutException e) {
411             // expected
412         }
413 
414         // abort the connection
415         Assert.assertTrue(conn instanceof HttpClientConnection);
416         conn.shutdown();
417         this.connManager.releaseConnection(conn, null, -1, null);
418 
419         // the connection is expected to be released back to the manager
420         conn = getConnection(this.connManager, route, 5L, TimeUnit.SECONDS);
421         Assert.assertFalse("connection should have been closed", conn.isOpen());
422 
423         this.connManager.releaseConnection(conn, null, -1, null);
424         this.connManager.shutdown();
425     }
426 
427     @Test
428     public void testAbortDuringConnecting() throws Exception {
429         final CountDownLatch connectLatch = new CountDownLatch(1);
430         final StallingSocketFactory stallingSocketFactory = new StallingSocketFactory(
431                 connectLatch, WaitPolicy.BEFORE_CONNECT, PlainConnectionSocketFactory.getSocketFactory());
432         final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
433             .register("http", stallingSocketFactory)
434             .build();
435 
436         this.connManager = new PoolingHttpClientConnectionManager(registry);
437         this.clientBuilder.setConnectionManager(this.connManager);
438 
439         this.connManager.setMaxTotal(1);
440 
441         final HttpHost target = start();
442         final HttpRoute route = new HttpRoute(target, null, false);
443         final HttpContext context = new BasicHttpContext();
444 
445         final HttpClientConnection conn = getConnection(this.connManager, route);
446 
447         final AtomicReference<Throwable> throwRef = new AtomicReference<Throwable>();
448         final Thread abortingThread = new Thread(new Runnable() {
449             @Override
450             public void run() {
451                 try {
452                     stallingSocketFactory.waitForState();
453                     conn.shutdown();
454                     connManager.releaseConnection(conn, null, -1, null);
455                     connectLatch.countDown();
456                 } catch (final Throwable e) {
457                     throwRef.set(e);
458                 }
459             }
460         });
461         abortingThread.start();
462 
463         try {
464             this.connManager.connect(conn, route, 0, context);
465             this.connManager.routeComplete(conn, route, context);
466             Assert.fail("expected SocketException");
467         } catch(final SocketException expected) {}
468 
469         abortingThread.join(5000);
470         if(throwRef.get() != null) {
471             throw new RuntimeException(throwRef.get());
472         }
473 
474         Assert.assertFalse(conn.isOpen());
475 
476         // the connection is expected to be released back to the manager
477         final HttpClientConnection conn2 = getConnection(this.connManager, route, 5L, TimeUnit.SECONDS);
478         Assert.assertFalse("connection should have been closed", conn2.isOpen());
479 
480         this.connManager.releaseConnection(conn2, null, -1, null);
481         this.connManager.shutdown();
482     }
483 
484     @Test
485     public void testAbortBeforeSocketCreate() throws Exception {
486         final CountDownLatch connectLatch = new CountDownLatch(1);
487         final StallingSocketFactory stallingSocketFactory = new StallingSocketFactory(
488                 connectLatch, WaitPolicy.BEFORE_CREATE, PlainConnectionSocketFactory.getSocketFactory());
489         final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
490             .register("http", stallingSocketFactory)
491             .build();
492 
493         this.connManager = new PoolingHttpClientConnectionManager(registry);
494         this.clientBuilder.setConnectionManager(this.connManager);
495 
496         this.connManager.setMaxTotal(1);
497 
498         final HttpHost target = start();
499         final HttpRoute route = new HttpRoute(target, null, false);
500         final HttpContext context = new BasicHttpContext();
501 
502         final HttpClientConnection conn = getConnection(this.connManager, route);
503 
504         final AtomicReference<Throwable> throwRef = new AtomicReference<Throwable>();
505         final Thread abortingThread = new Thread(new Runnable() {
506             @Override
507             public void run() {
508                 try {
509                     stallingSocketFactory.waitForState();
510                     conn.shutdown();
511                     connManager.releaseConnection(conn, null, -1, null);
512                     connectLatch.countDown();
513                 } catch (final Throwable e) {
514                     throwRef.set(e);
515                 }
516             }
517         });
518         abortingThread.start();
519 
520         try {
521             this.connManager.connect(conn, route, 0, context);
522             this.connManager.routeComplete(conn, route, context);
523             Assert.fail("IOException expected");
524         } catch(final IOException expected) {
525         }
526 
527         abortingThread.join(5000);
528         if(throwRef.get() != null) {
529             throw new RuntimeException(throwRef.get());
530         }
531 
532         Assert.assertFalse(conn.isOpen());
533 
534         // the connection is expected to be released back to the manager
535         final HttpClientConnection conn2 = getConnection(this.connManager, route, 5L, TimeUnit.SECONDS);
536         Assert.assertFalse("connection should have been closed", conn2.isOpen());
537 
538         this.connManager.releaseConnection(conn2, null, -1, null);
539         this.connManager.shutdown();
540     }
541 
542     @Test
543     public void testAbortAfterSocketConnect() throws Exception {
544         final CountDownLatch connectLatch = new CountDownLatch(1);
545         final StallingSocketFactory stallingSocketFactory = new StallingSocketFactory(
546                 connectLatch, WaitPolicy.AFTER_CONNECT, PlainConnectionSocketFactory.getSocketFactory());
547         final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
548             .register("http", stallingSocketFactory)
549             .build();
550 
551         this.connManager = new PoolingHttpClientConnectionManager(registry);
552         this.clientBuilder.setConnectionManager(this.connManager);
553 
554         this.connManager.setMaxTotal(1);
555 
556         final HttpHost target = start();
557         final HttpRoute route = new HttpRoute(target, null, false);
558         final HttpContext context = new BasicHttpContext();
559 
560         final HttpClientConnection conn = getConnection(this.connManager, route);
561 
562         final AtomicReference<Throwable> throwRef = new AtomicReference<Throwable>();
563         final Thread abortingThread = new Thread(new Runnable() {
564             @Override
565             public void run() {
566                 try {
567                     stallingSocketFactory.waitForState();
568                     conn.shutdown();
569                     connManager.releaseConnection(conn, null, -1, null);
570                     connectLatch.countDown();
571                 } catch (final Throwable e) {
572                     throwRef.set(e);
573                 }
574             }
575         });
576         abortingThread.start();
577 
578         try {
579             this.connManager.connect(conn, route, 0, context);
580             this.connManager.routeComplete(conn, route, context);
581             Assert.fail("IOException expected");
582         } catch(final IOException expected) {
583         }
584 
585         abortingThread.join(5000);
586         if(throwRef.get() != null) {
587             throw new RuntimeException(throwRef.get());
588         }
589 
590         Assert.assertFalse(conn.isOpen());
591 
592         // the connection is expected to be released back to the manager
593         final HttpClientConnection conn2 = getConnection(this.connManager, route, 5L, TimeUnit.SECONDS);
594         Assert.assertFalse("connection should have been closed", conn2.isOpen());
595 
596         this.connManager.releaseConnection(conn2, null, -1, null);
597         this.connManager.shutdown();
598     }
599 
600     static class LatchSupport {
601 
602         private final CountDownLatch continueLatch;
603         private final CountDownLatch waitLatch = new CountDownLatch(1);
604         protected final WaitPolicy waitPolicy;
605 
606         LatchSupport(final CountDownLatch continueLatch, final WaitPolicy waitPolicy) {
607             this.continueLatch = continueLatch;
608             this.waitPolicy = waitPolicy;
609         }
610 
611         void waitForState() throws InterruptedException {
612             if(!waitLatch.await(1, TimeUnit.SECONDS)) {
613                 throw new RuntimeException("waited too long");
614             }
615         }
616 
617         void latch() {
618             waitLatch.countDown();
619             try {
620                 if (!continueLatch.await(60, TimeUnit.SECONDS)) {
621                     throw new RuntimeException("waited too long!");
622                 }
623             } catch (final InterruptedException e) {
624                 throw new RuntimeException(e);
625             }
626         }
627     }
628 
629     private static class StallingSocketFactory extends LatchSupport implements ConnectionSocketFactory {
630 
631         private final ConnectionSocketFactory delegate;
632 
633         public StallingSocketFactory(
634                 final CountDownLatch continueLatch,
635                 final WaitPolicy waitPolicy,
636                 final ConnectionSocketFactory delegate) {
637             super(continueLatch, waitPolicy);
638             this.delegate = delegate;
639         }
640 
641         @Override
642         public Socket connectSocket(
643                 final int connectTimeout,
644                 final Socket sock,
645                 final HttpHost host,
646                 final InetSocketAddress remoteAddress,
647                 final InetSocketAddress localAddress,
648                 final HttpContext context) throws IOException, ConnectTimeoutException {
649             if(waitPolicy == WaitPolicy.BEFORE_CONNECT) {
650                 latch();
651             }
652 
653             final Socket socket = delegate.connectSocket(
654                     connectTimeout, sock, host, remoteAddress, localAddress, context);
655 
656             if(waitPolicy == WaitPolicy.AFTER_CONNECT) {
657                 latch();
658             }
659 
660             return socket;
661         }
662 
663         @Override
664         public Socket createSocket(final HttpContext context) throws IOException {
665             if(waitPolicy == WaitPolicy.BEFORE_CREATE) {
666                 latch();
667             }
668 
669             return delegate.createSocket(context);
670         }
671 
672     }
673 
674     private enum WaitPolicy { BEFORE_CREATE, BEFORE_CONNECT, AFTER_CONNECT, AFTER_OPEN }
675 
676 }