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