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:31 UTC

[maven-wagon] branch WAGON-526 created (now f920bc1)

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

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


      at f920bc1  [WAGON-526] Make the retry handling of HttpClient configurable

This branch includes the following new commits:

     new f920bc1  [WAGON-526] Make the retry handling of HttpClient configurable

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



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

Posted by mi...@apache.org.
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 );
+        }
+    }
 }