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 <rmannibu...@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 );
+        }
+    }
 }

Reply via email to