diff --git a/LICENSE-commons-codec.txt b/3rd-party-licenses/LICENSE-commons-codec.txt similarity index 100% rename from LICENSE-commons-codec.txt rename to 3rd-party-licenses/LICENSE-commons-codec.txt diff --git a/LICENSE-httpclient.txt b/3rd-party-licenses/LICENSE-httpclient.txt similarity index 100% rename from LICENSE-httpclient.txt rename to 3rd-party-licenses/LICENSE-httpclient.txt diff --git a/LICENSE-httpcore.txt b/3rd-party-licenses/LICENSE-httpcore.txt similarity index 100% rename from LICENSE-httpcore.txt rename to 3rd-party-licenses/LICENSE-httpcore.txt diff --git a/LICENSE-jersey-apache-client4.txt b/3rd-party-licenses/LICENSE-jersey-apache-client4.txt similarity index 100% rename from LICENSE-jersey-apache-client4.txt rename to 3rd-party-licenses/LICENSE-jersey-apache-client4.txt diff --git a/LICENSE-jersey-client.txt b/3rd-party-licenses/LICENSE-jersey-client.txt similarity index 100% rename from LICENSE-jersey-client.txt rename to 3rd-party-licenses/LICENSE-jersey-client.txt diff --git a/LICENSE-jersey-core.txt b/3rd-party-licenses/LICENSE-jersey-core.txt similarity index 100% rename from LICENSE-jersey-core.txt rename to 3rd-party-licenses/LICENSE-jersey-core.txt diff --git a/LICENSE-log4j.txt b/3rd-party-licenses/LICENSE-log4j.txt similarity index 100% rename from LICENSE-log4j.txt rename to 3rd-party-licenses/LICENSE-log4j.txt diff --git a/build.gradle b/build.gradle index 0cab1a5..243f599 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ description = 'Smart REST Client - JAX-RS (Jersey) REST client that provides cli ext.githubProjectName = 'smart-client-java' buildscript { - ext.commonBuildVersion = '1.3.2' + ext.commonBuildVersion = '1.3.3' ext.commonBuildDir = "https://raw.githubusercontent.com/emcvipr/ecs-common-build/v$commonBuildVersion" apply from: "$commonBuildDir/ecs-publish.buildscript.gradle", to: buildscript } @@ -39,7 +39,7 @@ apply from: "$commonBuildDir/ecs-publish.gradle" dependencies { compile 'com.sun.jersey:jersey-client:1.19', 'com.sun.jersey.contribs:jersey-apache-client4:1.19', - 'org.apache.httpcomponents:httpclient:4.5', + 'org.apache.httpcomponents:httpclient:4.2.6', 'log4j:log4j:1.2.17' testCompile 'junit:junit:4.12' } diff --git a/src/main/java/com/emc/rest/smart/HostListProvider.java b/src/main/java/com/emc/rest/smart/HostListProvider.java index 2f34080..eb1a10c 100644 --- a/src/main/java/com/emc/rest/smart/HostListProvider.java +++ b/src/main/java/com/emc/rest/smart/HostListProvider.java @@ -37,4 +37,13 @@ public interface HostListProvider { * (host.setHealthy(false) is called). */ void runHealthCheck(Host host); + + /** + * Destroy this provider. Any system resources associated with the provider + * will be cleaned up. + *

+ * The provider must not be reused after this method is called otherwise + * undefined behavior will occur. + */ + void destroy(); } diff --git a/src/main/java/com/emc/rest/smart/LoadBalancer.java b/src/main/java/com/emc/rest/smart/LoadBalancer.java index 1df21e3..97e4d34 100644 --- a/src/main/java/com/emc/rest/smart/LoadBalancer.java +++ b/src/main/java/com/emc/rest/smart/LoadBalancer.java @@ -42,9 +42,9 @@ public LoadBalancer(List initialHosts) { * Returns the host with the lowest response index. */ public Host getTopHost(Map requestProperties) { - Host topHost = null; + Host topHost = null, topHealthyHost = null; - long lowestIndex = Long.MAX_VALUE; + long lowestIndex = Long.MAX_VALUE, lowestHealthyIndex = Long.MAX_VALUE; synchronized (hosts) { for (Host host : hosts) { @@ -52,9 +52,6 @@ public Host getTopHost(Map requestProperties) { // apply any veto rules if (shouldVeto(host, requestProperties)) continue; - // if the host is unhealthy/down, ignore it - if (!host.isHealthy()) continue; - // get response index for a host long hostIndex = host.getResponseIndex(); @@ -63,8 +60,17 @@ public Host getTopHost(Map requestProperties) { topHost = host; lowestIndex = hostIndex; } + + // also keep track of the top *healthy* host + if (host.isHealthy() && hostIndex < lowestHealthyIndex) { + topHealthyHost = host; + lowestHealthyIndex = hostIndex; + } } + // if there are no healthy hosts, we still need a host to contact + if (topHealthyHost != null) topHost = topHealthyHost; + // move the top host to the end of the host list as an extra tie-breaker hosts.remove(topHost); hosts.add(topHost); diff --git a/src/main/java/com/emc/rest/smart/PollingDaemon.java b/src/main/java/com/emc/rest/smart/PollingDaemon.java index a37944e..3bcf881 100644 --- a/src/main/java/com/emc/rest/smart/PollingDaemon.java +++ b/src/main/java/com/emc/rest/smart/PollingDaemon.java @@ -99,4 +99,12 @@ public void run() { public void terminate() { running = false; } + + public SmartConfig getSmartConfig() { + return smartConfig; + } + + public boolean isRunning() { + return running; + } } diff --git a/src/main/java/com/emc/rest/smart/SmartClientFactory.java b/src/main/java/com/emc/rest/smart/SmartClientFactory.java index 4d9432b..951c3da 100644 --- a/src/main/java/com/emc/rest/smart/SmartClientFactory.java +++ b/src/main/java/com/emc/rest/smart/SmartClientFactory.java @@ -36,9 +36,16 @@ import com.sun.jersey.core.impl.provider.entity.ByteArrayProvider; import com.sun.jersey.core.impl.provider.entity.FileProvider; import com.sun.jersey.core.impl.provider.entity.InputStreamProvider; +import org.apache.http.impl.client.AbstractHttpClient; +import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; import org.apache.http.impl.conn.PoolingClientConnectionManager; +import org.apache.log4j.Logger; public final class SmartClientFactory { + private static final Logger l4j = Logger.getLogger(SmartClientFactory.class); + + public static final String DISABLE_APACHE_RETRY = "com.emc.rest.smart.disableApacheRetry"; + public static Client createSmartClient(SmartConfig smartConfig) { return createSmartClient(smartConfig, createApacheClientHandler(smartConfig)); } @@ -96,9 +103,31 @@ public static Client createStandardClient(SmartConfig smartConfig, clientConfig.getClasses().add(InputStreamProvider.class); // build Jersey client - Client client = new Client(clientHandler, clientConfig); + return new Client(clientHandler, clientConfig); + } - return client; + /** + * Destroy this client. Any system resources associated with the client + * will be cleaned up. + *

+ * This method must be called when there are not responses pending otherwise + * undefined behavior will occur. + *

+ * The client must not be reused after this method is called otherwise + * undefined behavior will occur. + */ + public static void destroy(Client client) { + PollingDaemon pollingDaemon = (PollingDaemon) client.getProperties().get(PollingDaemon.PROPERTY_KEY); + if (pollingDaemon != null) { + l4j.debug("terminating polling daemon"); + pollingDaemon.terminate(); + if (pollingDaemon.getSmartConfig().getHostListProvider() != null) { + l4j.debug("destroying host list provider"); + pollingDaemon.getSmartConfig().getHostListProvider().destroy(); + } + } + l4j.debug("destroying Jersey client"); + client.destroy(); } static ApacheHttpClient4Handler createApacheClientHandler(SmartConfig smartConfig) { @@ -119,7 +148,13 @@ static ApacheHttpClient4Handler createApacheClientHandler(SmartConfig smartConfi if (smartConfig.getProxyPass() != null) clientConfig.getProperties().put(ApacheHttpClient4Config.PROPERTY_PROXY_PASSWORD, smartConfig.getProxyPass()); - return ApacheHttpClient4.create(clientConfig).getClientHandler(); + ApacheHttpClient4Handler handler = ApacheHttpClient4.create(clientConfig).getClientHandler(); + + // disable the retry handler if necessary + if (smartConfig.getProperty(DISABLE_APACHE_RETRY) != null) + ((AbstractHttpClient) handler.getHttpClient()).setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(0, false)); + + return handler; } private SmartClientFactory() { diff --git a/src/main/java/com/emc/rest/smart/SmartConfig.java b/src/main/java/com/emc/rest/smart/SmartConfig.java index f234577..306c7db 100644 --- a/src/main/java/com/emc/rest/smart/SmartConfig.java +++ b/src/main/java/com/emc/rest/smart/SmartConfig.java @@ -146,7 +146,7 @@ public Object getProperty(String propName) { /** * Allows custom Jersey client properties to be set. These will be passed on in the Jersey ClientConfig */ - public void withProperty(String propName, Object value) { + public void setProperty(String propName, Object value) { properties.put(propName, value); } @@ -184,4 +184,9 @@ public SmartConfig withHealthCheckEnabled(boolean healthCheckEnabled) { setHealthCheckEnabled(healthCheckEnabled); return this; } + + public SmartConfig withProperty(String propName, Object value) { + setProperty(propName, value); + return this; + } } diff --git a/src/main/java/com/emc/rest/smart/ecs/EcsHostListProvider.java b/src/main/java/com/emc/rest/smart/ecs/EcsHostListProvider.java index 820bf82..8ac229d 100644 --- a/src/main/java/com/emc/rest/smart/ecs/EcsHostListProvider.java +++ b/src/main/java/com/emc/rest/smart/ecs/EcsHostListProvider.java @@ -102,6 +102,11 @@ public void runHealthCheck(Host host) { client.resource(getRequestUri(host, "/?ping")).header("x-emc-namespace", "x").get(String.class); } + @Override + public void destroy() { + client.destroy(); + } + protected List getDataNodes(Host host) { String path = "/?endpoint"; URI uri = getRequestUri(host, path); diff --git a/src/test/java/com/emc/rest/smart/TestHealthCheck.java b/src/test/java/com/emc/rest/smart/TestHealthCheck.java index 80a626d..aee4c11 100644 --- a/src/test/java/com/emc/rest/smart/TestHealthCheck.java +++ b/src/test/java/com/emc/rest/smart/TestHealthCheck.java @@ -140,6 +140,10 @@ public TestHostListProvider(Host host, boolean healthy) { this.healthy = healthy; } + @Override + public void destroy() { + } + @Override public List getHostList() { throw new RuntimeException("no host update");