You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by mi...@apache.org on 2018/08/24 17:00:32 UTC

[maven-wagon] 01/01: [WAGON-526] Make the retry handling of HttpClient configurable

This is an automated email from the ASF dual-hosted git repository.

michaelo pushed a commit to branch WAGON-526
in repository https://gitbox.apache.org/repos/asf/maven-wagon.git

commit f920bc1a4d0f407453d0c45b5cf39316d9eb19d3
Author: Romain Manni-Bucau <rm...@gmail.com>
AuthorDate: Mon Aug 20 10:55:23 2018 +0200

    [WAGON-526] Make the retry handling of HttpClient configurable
    
    This closes #37
---
 .../wagon/shared/http/AbstractHttpClientWagon.java |  94 +++++++++
 wagon-providers/wagon-http/src/site/apt/index.apt  |  21 +-
 .../http/AbstractHttpClientWagonTest.java          | 221 +++++++++++++++++++++
 3 files changed, 335 insertions(+), 1 deletion(-)

diff --git a/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/AbstractHttpClientWagon.java b/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/AbstractHttpClientWagon.java
index 6c47557..63d848a 100755
--- a/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/AbstractHttpClientWagon.java
+++ b/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/AbstractHttpClientWagon.java
@@ -32,6 +32,7 @@ import org.apache.http.auth.NTCredentials;
 import org.apache.http.auth.UsernamePasswordCredentials;
 import org.apache.http.client.AuthCache;
 import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.HttpRequestRetryHandler;
 import org.apache.http.client.config.CookieSpecs;
 import org.apache.http.client.config.RequestConfig;
 import org.apache.http.client.methods.CloseableHttpResponse;
@@ -54,7 +55,9 @@ import org.apache.http.impl.auth.BasicScheme;
 import org.apache.http.impl.client.BasicAuthCache;
 import org.apache.http.impl.client.BasicCredentialsProvider;
 import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
 import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.StandardHttpRequestRetryHandler;
 import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
 import org.apache.http.message.BasicHeader;
 import org.apache.http.protocol.HTTP;
@@ -82,7 +85,10 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Date;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Properties;
@@ -354,6 +360,93 @@ public abstract class AbstractHttpClientWagon
         return connManager;
     }
 
+    /**
+     * The type of the retry handler, defaults to {@code standard}.
+     * Values can be {@link default DefaultHttpRequestRetryHandler},
+     * or {@link standard StandardHttpRequestRetryHandler},
+     * or a fully qualified name class with a no-arg.
+     *
+     * @since 3.2
+     */
+    private static final String RETRY_HANDLER_CLASS =
+            System.getProperty( "maven.wagon.http.retryHandler.class", "standard" );
+
+    /**
+     * Whether or not methods that have successfully sent their request will be retried,
+     * defaults to {@code false}.
+     * Note: only used for default and standard retry handlers.
+     *
+     * @since 3.2
+     */
+    private static final boolean RETRY_HANDLER_REQUEST_SENT_ENABLED =
+            Boolean.getBoolean( "maven.wagon.http.retryHandler.requestSentEnabled" );
+
+    /**
+     * Number of retries for the retry handler, defaults to 3.
+     * Note: only used for default and standard retry handlers.
+     *
+     * @since 3.2
+     */
+    private static final int RETRY_HANDLER_COUNT =
+            Integer.getInteger( "maven.wagon.http.retryHandler.count", 3 );
+
+    /**
+     * Comma-separated list of non-retryable exception classes.
+     * Note: only used for default retry handler.
+     *
+     * @since 3.2
+     */
+    private static final String RETRY_HANDLER_EXCEPTIONS =
+            System.getProperty( "maven.wagon.http.retryHandler.nonRetryableClasses" );
+
+    private static HttpRequestRetryHandler createRetryHandler()
+    {
+        switch ( RETRY_HANDLER_CLASS )
+        {
+            case "default":
+                if ( StringUtils.isEmpty( RETRY_HANDLER_EXCEPTIONS ) )
+                {
+                    return new DefaultHttpRequestRetryHandler(
+                            RETRY_HANDLER_COUNT, RETRY_HANDLER_REQUEST_SENT_ENABLED );
+                }
+                return new DefaultHttpRequestRetryHandler(
+                        RETRY_HANDLER_COUNT, RETRY_HANDLER_REQUEST_SENT_ENABLED, getNonRetryableExceptions() )
+                {
+                };
+            case "standard":
+                return new StandardHttpRequestRetryHandler( RETRY_HANDLER_COUNT, RETRY_HANDLER_REQUEST_SENT_ENABLED );
+            default:
+                try
+                {
+                    final ClassLoader classLoader = AbstractHttpClientWagon.class.getClassLoader();
+                    return HttpRequestRetryHandler.class.cast( classLoader.loadClass( RETRY_HANDLER_CLASS )
+                                                                          .getConstructor().newInstance() );
+                }
+                catch ( final Exception e )
+                {
+                    throw new IllegalArgumentException( e );
+                }
+        }
+    }
+
+    private static Collection<Class<? extends IOException>> getNonRetryableExceptions()
+    {
+        final List<Class<? extends IOException>> exceptions = new ArrayList<>();
+        final ClassLoader loader = AbstractHttpClientWagon.class.getClassLoader();
+        for ( final String ex : RETRY_HANDLER_EXCEPTIONS.split( "," ) )
+        {
+            try
+            {
+                exceptions.add( ( Class<? extends IOException> ) loader.loadClass( ex ) );
+            }
+            catch ( final ClassNotFoundException e )
+            {
+                throw new IllegalArgumentException( e );
+            }
+        }
+        return exceptions;
+    }
+
     private static CloseableHttpClient httpClient = createClient();
 
     private static CloseableHttpClient createClient()
@@ -362,6 +455,7 @@ public abstract class AbstractHttpClientWagon
             .useSystemProperties() //
             .disableConnectionState() //
             .setConnectionManager( httpClientConnectionManager ) //
+            .setRetryHandler( createRetryHandler() )
             .build();
     }
 
diff --git a/wagon-providers/wagon-http/src/site/apt/index.apt b/wagon-providers/wagon-http/src/site/apt/index.apt
index 732af58..12ccf46 100644
--- a/wagon-providers/wagon-http/src/site/apt/index.apt
+++ b/wagon-providers/wagon-http/src/site/apt/index.apt
@@ -31,7 +31,7 @@ Maven Wagon HTTP
 
  This component is an implementation of Wagon provider for HTTP access.
  It uses {{{http://hc.apache.org/httpcomponents-client-ga/}Apache HttpComponents client}} as lower level layer.
- 
+
  It enables Maven to use remote repositories stored in HTTP servers.
 
 
@@ -57,3 +57,22 @@ Features
  * <<<maven.wagon.http.ssl.ignore.validity.dates>>> = true/false (default false), ignore issues with certificate dates.
 
  * <<<maven.wagon.rto>>> = time in ms (default 1800000), read time out.
+
+ []
+
+ Since version 3.2, the retry handler can be configured with system properties:
+
+ * <<<maven.wagon.http.retryHandler.class>>> supports this set of values:
+
+    * <<<default>>> will use an instance of {{{http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/client/DefaultHttpRequestRetryHandler.html}<<<DefaultHttpRequestRetryHandler>>>}} respecting <<<requestSentEnabled>>>, <<<count>>> and <<<nonRetryableClasses>>>.
+
+    *<< <standard>>> will use an instance of {{{http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/client/StandardHttpRequestRetryHandler.html}<<<StandardHttpRequestRetryHandler>>>}} respecting <<<requestSentEnabled>>> and <<<count>>>.
+
+    * Any fully qualified name of a {{{https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/client/HttpRequestRetryHandler.html}<<<HttpRequestRetryHandler>>>}} implementation will be instantiated with its default constructor.
+
+ * <<<maven.wagon.http.retryHandler.requestSentEnabled>>> = <<<requestSentEnabled>>> for <<<default>>> or <<<standard>>> implementations.
+
+ * <<<maven.wagon.http.retryHandler.count>>> = number of retries for <<<default>>> or <<<standard>>> implementations.
+
+ * <<<maven.wagon.http.retryHandler.nonRetryableClasses>>> = a comma-separated list of fully qualified class names bypassing the retries (only the <<<default>>> implementation).
+ If not set, the default value from {{{http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/client/DefaultHttpRequestRetryHandler.html}<<<DefaultHttpRequestRetryHandler>>>}} will be used.
diff --git a/wagon-providers/wagon-http/src/test/java/org/apache/maven/wagon/providers/http/AbstractHttpClientWagonTest.java b/wagon-providers/wagon-http/src/test/java/org/apache/maven/wagon/providers/http/AbstractHttpClientWagonTest.java
index e4091ec..8eb8233 100644
--- a/wagon-providers/wagon-http/src/test/java/org/apache/maven/wagon/providers/http/AbstractHttpClientWagonTest.java
+++ b/wagon-providers/wagon-http/src/test/java/org/apache/maven/wagon/providers/http/AbstractHttpClientWagonTest.java
@@ -19,6 +19,34 @@ package org.apache.maven.wagon.providers.http;
  * under the License.
  */
 
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.lang.reflect.Field;
+import java.net.ConnectException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Set;
+
+import javax.net.ssl.SSLException;
+
+import org.apache.http.client.HttpRequestRetryHandler;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
+import org.apache.http.impl.execchain.RedirectExec;
+import org.apache.http.impl.execchain.RetryExec;
 import org.apache.maven.wagon.InputData;
 import org.apache.maven.wagon.repository.Repository;
 import org.apache.maven.wagon.resource.Resource;
@@ -51,4 +79,197 @@ public class AbstractHttpClientWagonTest
 
         wagon.disconnect();
     }
+
+    @Test
+    public void retryableConfigurationDefaultTest() throws Exception
+    {
+        doTestHttpClient( new Runnable()
+        {
+            @Override
+            public void run()
+            {
+                final HttpRequestRetryHandler handler = getCurrentHandler();
+                assertNotNull( handler );
+                assertThat( handler, instanceOf( DefaultHttpRequestRetryHandler.class ) );
+                final DefaultHttpRequestRetryHandler impl = DefaultHttpRequestRetryHandler.class.cast(handler);
+                assertEquals( 3, impl.getRetryCount() );
+                assertFalse( impl.isRequestSentRetryEnabled() );
+            }
+        });
+    }
+
+    @Test
+    public void retryableConfigurationCountTest() throws Exception
+    {
+        doTestHttpClient( new Runnable()
+        {
+            @Override
+            public void run()
+            {
+                System.setProperty( "maven.wagon.http.retryHandler.class", "default" );
+                System.setProperty( "maven.wagon.http.retryHandler.count", "5" );
+
+                final HttpRequestRetryHandler handler = getCurrentHandler();
+                assertNotNull( handler );
+                assertThat( handler, instanceOf( DefaultHttpRequestRetryHandler.class ) );
+                final DefaultHttpRequestRetryHandler impl = DefaultHttpRequestRetryHandler.class.cast(handler);
+                assertEquals( 5, impl.getRetryCount() );
+                assertFalse( impl.isRequestSentRetryEnabled() );
+            }
+        });
+    }
+
+    @Test
+    public void retryableConfigurationSentTest() throws Exception
+    {
+        doTestHttpClient( new Runnable()
+        {
+            @Override
+            public void run()
+            {
+                System.setProperty( "maven.wagon.http.retryHandler.class", "default" );
+                System.setProperty( "maven.wagon.http.retryHandler.requestSentEnabled", "true" );
+
+                final HttpRequestRetryHandler handler = getCurrentHandler();
+                assertNotNull( handler );
+                assertThat( handler, instanceOf( DefaultHttpRequestRetryHandler.class ) );
+                final DefaultHttpRequestRetryHandler impl = DefaultHttpRequestRetryHandler.class.cast(handler);
+                assertEquals( 3, impl.getRetryCount() );
+                assertTrue( impl.isRequestSentRetryEnabled() );
+            }
+        });
+    }
+
+    @Test
+    public void retryableConfigurationExceptionsTest() throws Exception
+    {
+        doTestHttpClient( new Runnable()
+        {
+            @Override
+            public void run()
+            {
+                System.setProperty( "maven.wagon.http.retryHandler.class", "default" );
+                System.setProperty( "maven.wagon.http.retryHandler.nonRetryableClasses", IOException.class.getName() );
+
+                final HttpRequestRetryHandler handler = getCurrentHandler();
+                assertNotNull( handler );
+                assertThat( handler, instanceOf( DefaultHttpRequestRetryHandler.class ) );
+                final DefaultHttpRequestRetryHandler impl = DefaultHttpRequestRetryHandler.class.cast(handler);
+                assertEquals( 3, impl.getRetryCount() );
+                assertFalse( impl.isRequestSentRetryEnabled() );
+
+                try
+                {
+                    final Field nonRetriableClasses = handler.getClass().getSuperclass()
+                            .getDeclaredField( "nonRetriableClasses" );
+                    if ( !nonRetriableClasses.isAccessible() )
+                    {
+                        nonRetriableClasses.setAccessible(true);
+                    }
+                    final Set<?> exceptions = Set.class.cast( nonRetriableClasses.get(handler) );
+                    assertEquals( 1, exceptions.size() );
+                    assertTrue( exceptions.contains( IOException.class ) );
+                }
+                catch ( final Exception e )
+                {
+                    fail( e.getMessage() );
+                }
+            }
+        });
+    }
+
+    private HttpRequestRetryHandler getCurrentHandler()
+    {
+        try
+        {
+            final Class<?> impl = Thread.currentThread().getContextClassLoader().loadClass(
+                        "org.apache.maven.wagon.shared.http.AbstractHttpClientWagon" );
+
+            final CloseableHttpClient httpClient = CloseableHttpClient.class.cast(
+                    impl.getMethod("getHttpClient").invoke(null) );
+
+            final Field redirectExec = httpClient.getClass().getDeclaredField( "execChain" );
+            if ( !redirectExec.isAccessible() )
+            {
+                redirectExec.setAccessible( true );
+            }
+            final RedirectExec redirectExecInstance = RedirectExec.class.cast(
+                    redirectExec.get( httpClient ) );
+
+            final Field requestExecutor = redirectExecInstance.getClass().getDeclaredField( "requestExecutor" );
+            if ( !requestExecutor.isAccessible() )
+            {
+                requestExecutor.setAccessible( true );
+            }
+            final RetryExec requestExecutorInstance = RetryExec.class.cast(
+                    requestExecutor.get( redirectExecInstance ) );
+
+            final Field retryHandler = requestExecutorInstance.getClass().getDeclaredField( "retryHandler" );
+            if ( !retryHandler.isAccessible() )
+            {
+                retryHandler.setAccessible( true );
+            }
+            return HttpRequestRetryHandler.class.cast( retryHandler.get( requestExecutorInstance ) );
+        }
+        catch ( final Exception e )
+        {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    private void doTestHttpClient( final Runnable test ) throws Exception
+    {
+        final String classpath = System.getProperty( "java.class.path" );
+        final String[] paths = classpath.split( File.pathSeparator );
+        final Collection<URL> urls = new ArrayList<>( paths.length );
+        for ( final String path : paths )
+        {
+            try
+            {
+                urls.add( new File( path ).toURI().toURL() );
+            }
+            catch ( final MalformedURLException e )
+            {
+                fail( e.getMessage() );
+            }
+        }
+        final URLClassLoader loader = new URLClassLoader( urls.toArray( new URL[ paths.length ] ) , new ClassLoader()
+        {
+            @Override
+            protected Class<?> loadClass( final String name, final boolean resolve ) throws ClassNotFoundException
+            {
+                if ( name.startsWith( "org.apache.maven.wagon.shared.http" ) )
+                {
+                    throw new ClassNotFoundException( name );
+                }
+                return super.loadClass( name, resolve );
+            }
+        });
+        final Thread thread = Thread.currentThread();
+        final ClassLoader contextClassLoader = thread.getContextClassLoader();
+        thread.setContextClassLoader( loader );
+
+        final String originalClass = System.getProperty( "maven.wagon.http.retryHandler.class", "default" );
+        final String originalSentEnabled = System.getProperty(
+                "maven.wagon.http.retryHandler.requestSentEnabled", "false" );
+        final String originalCount = System.getProperty( "maven.wagon.http.retryHandler.count", "3" );
+        final String originalExceptions = System.getProperty( "maven.wagon.http.retryHandler.nonRetryableClasses",
+                InterruptedIOException.class.getName() + ","
+                    + UnknownHostException.class.getName() + ","
+                    + ConnectException.class.getName() + ","
+                    + SSLException.class.getName());
+        try
+        {
+            test.run();
+        }
+        finally
+        {
+            loader.close();
+            thread.setContextClassLoader( contextClassLoader );
+            System.setProperty(  "maven.wagon.http.retryHandler.class", originalClass );
+            System.setProperty(  "maven.wagon.http.retryHandler.requestSentEnabled", originalSentEnabled );
+            System.setProperty(  "maven.wagon.http.retryHandler.count", originalCount );
+            System.setProperty(  "maven.wagon.http.retryHandler.nonRetryableClasses", originalExceptions );
+        }
+    }
 }