This is an automated email from the ASF dual-hosted git repository. more pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/knox.git
The following commit(s) were added to refs/heads/master by this push: new 673bb4b KNOX-843 - Client side HA (#380) 673bb4b is described below commit 673bb4b9532fba3f4ad9b6238340b787109f723c Author: Sandeep Moré <moresand...@gmail.com> AuthorDate: Fri Oct 23 13:49:17 2020 -0400 KNOX-843 - Client side HA (#380) * KNOX-843 - Client side HA * KNOX-843 - Added enableLoadBalancing config, updated variables names to enableStickySession and stickySessionCookieName --- gateway-provider-ha/pom.xml | 6 +- .../gateway/ha/dispatch/DefaultHaDispatch.java | 170 ++++++++++++++++++- .../ha/dispatch/i18n/HaDispatchMessages.java | 3 + .../knox/gateway/ha/provider/HaProvider.java | 15 ++ .../knox/gateway/ha/provider/HaServiceConfig.java | 16 ++ .../knox/gateway/ha/provider/URLManager.java | 2 + .../ha/provider/impl/BaseZookeeperURLManager.java | 9 +- .../ha/provider/impl/DefaultHaProvider.java | 25 ++- .../ha/provider/impl/DefaultHaServiceConfig.java | 48 ++++++ .../ha/provider/impl/DefaultURLManager.java | 9 +- .../ha/provider/impl/HaDescriptorConstants.java | 8 + .../ha/provider/impl/HaDescriptorFactory.java | 31 +++- .../ha/provider/impl/HaDescriptorManager.java | 12 +- .../ha/provider/impl/HaServiceConfigConstants.java | 16 ++ .../gateway/ha/dispatch/DefaultHaDispatchTest.java | 183 ++++++++++++++++++++- .../ha/provider/impl/HaDescriptorFactoryTest.java | 62 ++++++- .../ha/provider/impl/HaDescriptorManagerTest.java | 50 +++++- .../knox/gateway/rm/dispatch/RMHaDispatchTest.java | 4 +- .../hdfs/dispatch/WebHdfsHaDispatchTest.java | 2 +- .../knox/gateway/dispatch/DefaultDispatch.java | 41 ++++- 20 files changed, 674 insertions(+), 38 deletions(-) diff --git a/gateway-provider-ha/pom.xml b/gateway-provider-ha/pom.xml index ea5da54..4fedd20 100644 --- a/gateway-provider-ha/pom.xml +++ b/gateway-provider-ha/pom.xml @@ -49,7 +49,11 @@ <groupId>org.apache.knox</groupId> <artifactId>gateway-util-configinjector</artifactId> </dependency> - + + <dependency> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> + </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/DefaultHaDispatch.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/DefaultHaDispatch.java index b18c25d..0e479b1 100644 --- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/DefaultHaDispatch.java +++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/DefaultHaDispatch.java @@ -17,7 +17,13 @@ */ package org.apache.knox.gateway.ha.dispatch; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.utils.URIBuilder; import org.apache.knox.gateway.config.Configure; +import org.apache.knox.gateway.config.GatewayConfig; import org.apache.knox.gateway.dispatch.DefaultDispatch; import org.apache.knox.gateway.filter.AbstractGatewayFilter; import org.apache.knox.gateway.ha.dispatch.i18n.HaDispatchMessages; @@ -25,15 +31,23 @@ import org.apache.knox.gateway.ha.provider.HaProvider; import org.apache.knox.gateway.ha.provider.HaServiceConfig; import org.apache.knox.gateway.ha.provider.impl.HaServiceConfigConstants; import org.apache.knox.gateway.i18n.messages.MessagesFactory; -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpRequestBase; -import org.apache.http.client.methods.HttpUriRequest; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; /** * Default HA dispatch class that has a very basic failover mechanism @@ -44,10 +58,17 @@ public class DefaultHaDispatch extends DefaultDispatch { protected static final HaDispatchMessages LOG = MessagesFactory.get(HaDispatchMessages.class); - private int maxFailoverAttempts = HaServiceConfigConstants.DEFAULT_MAX_FAILOVER_ATTEMPTS; + private static final Map<String, String> urlToHashLookup = new HashMap<>(); + private static final Map<String, String> hashToUrlLookup = new HashMap<>(); + private int maxFailoverAttempts = HaServiceConfigConstants.DEFAULT_MAX_FAILOVER_ATTEMPTS; private int failoverSleep = HaServiceConfigConstants.DEFAULT_FAILOVER_SLEEP; + private boolean stickySessionsEnabled = HaServiceConfigConstants.DEFAULT_STICKY_SESSIONS_ENABLED; + private boolean loadBalancingEnabled = HaServiceConfigConstants.DEFAULT_LOAD_BALANCING_ENABLED; + private boolean noFallbackEnabled = HaServiceConfigConstants.DEFAULT_NO_FALLBACK_ENABLED; + private String stickySessionCookieName = HaServiceConfigConstants.DEFAULT_STICKY_SESSION_COOKIE_NAME; + private HaProvider haProvider; @Override @@ -58,6 +79,23 @@ public class DefaultHaDispatch extends DefaultDispatch { HaServiceConfig serviceConfig = haProvider.getHaDescriptor().getServiceConfig(getServiceRole()); maxFailoverAttempts = serviceConfig.getMaxFailoverAttempts(); failoverSleep = serviceConfig.getFailoverSleep(); + stickySessionsEnabled = serviceConfig.isStickySessionEnabled(); + loadBalancingEnabled = serviceConfig.isLoadBalancingEnabled(); + noFallbackEnabled = serviceConfig.isNoFallbackEnabled(); + stickySessionCookieName = serviceConfig.getStickySessionCookieName(); + setupUrlHashLookup(); + } + + // Suffix the cookie name by the service to make it unique + // The cookie path is NOT unique since Knox is stripping the service name. + stickySessionCookieName = stickySessionCookieName + '-' + getServiceRole(); + } + + private void setupUrlHashLookup() { + for (String url : haProvider.getURLs(getServiceRole())) { + String urlHash = hash(url); + urlToHashLookup.put(url, urlHash); + hashToUrlLookup.put(urlHash, url); } } @@ -71,6 +109,30 @@ public class DefaultHaDispatch extends DefaultDispatch { } @Override + protected void executeRequestWrapper(HttpUriRequest outboundRequest, + HttpServletRequest inboundRequest, HttpServletResponse outboundResponse) + throws IOException { + final Optional<URI> opt = setBackendfromHaCookie(outboundRequest, inboundRequest); + if(opt.isPresent()) { + ((HttpRequestBase) outboundRequest).setURI(opt.get()); + } + executeRequest(outboundRequest, inboundRequest, outboundResponse); + /** + * Load balance when + * 1. loadbalancing is enabled and sticky sessions are off + * 2. sticky sessions are enabled and it is a new session (no url in cookie) + */ + if ( (!opt.isPresent() && stickySessionsEnabled) || loadBalancingEnabled) { + haProvider.makeNextActiveURLAvailable(getServiceRole()); + } + } + + @Override + protected void outboundResponseWrapper(final HttpServletRequest inboundRequest, HttpServletResponse outboundResponse) { + setKnoxHaCookie(inboundRequest, outboundResponse); + } + + @Override protected void executeRequest(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest, HttpServletResponse outboundResponse) throws IOException { HttpResponse inboundResponse = null; try { @@ -82,8 +144,71 @@ public class DefaultHaDispatch extends DefaultDispatch { } } + private Optional<URI> setBackendfromHaCookie(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest) { + if (stickySessionsEnabled && inboundRequest.getCookies() != null) { + for (Cookie cookie : inboundRequest.getCookies()) { + if (stickySessionCookieName.equals(cookie.getName())) { + String backendURLHash = cookie.getValue(); + String backendURL = hashToUrlLookup.get(backendURLHash); + // Make sure that the url provided is actually a valid backend url + if (haProvider.getURLs(getServiceRole()).contains(backendURL)) { + try { + URI cookieUri = new URI(backendURL); + URIBuilder uriBuilder = new URIBuilder(outboundRequest.getURI()); + uriBuilder.setScheme(cookieUri.getScheme()); + uriBuilder.setHost(cookieUri.getHost()); + uriBuilder.setPort(cookieUri.getPort()); + URI uri = uriBuilder.build(); + return Optional.of(uri); + } catch (URISyntaxException ignore) { + // The cookie was invalid so we just don't set it. Knox will pick a backend automatically + } + } + } + } + } + return Optional.empty(); + } + + private void setKnoxHaCookie(HttpServletRequest inboundRequest, + HttpServletResponse outboundResponse) { + if (stickySessionsEnabled) { + List<Cookie> serviceHaCookies = Collections.emptyList(); + if(inboundRequest.getCookies() != null) { + serviceHaCookies = Arrays + .stream(inboundRequest.getCookies()) + .filter(cookie -> stickySessionCookieName.equals(cookie.getName())) + .collect(Collectors.toList()); + } + /* if the inbound request has a valid hash then no need to set a different hash */ + if (serviceHaCookies != null && !serviceHaCookies.isEmpty() + && hashToUrlLookup.containsKey(serviceHaCookies.get(0).getValue())) { + return; + } else { + String url = haProvider.getActiveURL(getServiceRole()); + String cookieValue = urlToHashLookup.get(url); + Cookie stickySessionCookie = new Cookie(stickySessionCookieName, cookieValue); + stickySessionCookie.setPath(inboundRequest.getContextPath()); + stickySessionCookie.setMaxAge(-1); + stickySessionCookie.setHttpOnly(true); + GatewayConfig config = (GatewayConfig) inboundRequest + .getServletContext() + .getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE); + if (config != null) { + stickySessionCookie.setSecure(config.isSSLEnabled()); + } + outboundResponse.addCookie(stickySessionCookie); + } + } + } protected void failoverRequest(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest, HttpServletResponse outboundResponse, HttpResponse inboundResponse, Exception exception) throws IOException { + /* check for a case where no fallback is configured */ + if(noFallbackEnabled && stickySessionsEnabled) { + LOG.noFallbackError(); + outboundResponse.sendError(HttpServletResponse.SC_BAD_GATEWAY, "Service connection error, HA failover disabled"); + return; + } LOG.failingOverRequest(outboundRequest.getURI().toString()); AtomicInteger counter = (AtomicInteger) inboundRequest.getAttribute(FAILOVER_COUNTER_ATTRIBUTE); if ( counter == null ) { @@ -92,8 +217,11 @@ public class DefaultHaDispatch extends DefaultDispatch { inboundRequest.setAttribute(FAILOVER_COUNTER_ATTRIBUTE, counter); if ( counter.incrementAndGet() <= maxFailoverAttempts ) { haProvider.markFailedURL(getServiceRole(), outboundRequest.getURI().toString()); + setupUrlHashLookup(); // refresh the url hash after failing a url //null out target url so that rewriters run again inboundRequest.setAttribute(AbstractGatewayFilter.TARGET_REQUEST_URL_ATTRIBUTE_NAME, null); + // Make sure to remove the cookie ha cookie from the request + inboundRequest = new StickySessionCookieRemovedRequest(stickySessionCookieName, inboundRequest); URI uri = getDispatchUrl(inboundRequest); ((HttpRequestBase) outboundRequest).setURI(uri); if ( failoverSleep > 0 ) { @@ -114,4 +242,38 @@ public class DefaultHaDispatch extends DefaultDispatch { } } } + + private String hash(String url) { + return DigestUtils.sha256Hex(url); + } + + /** + * Strips out the cookies by the cookie name provided + */ + private static class StickySessionCookieRemovedRequest extends HttpServletRequestWrapper { + private final Cookie[] cookies; + + StickySessionCookieRemovedRequest(String cookieName, HttpServletRequest request) { + super(request); + this.cookies = filterCookies(cookieName, request.getCookies()); + } + + private Cookie[] filterCookies(String cookieName, Cookie[] cookies) { + if (super.getCookies() == null) { + return null; + } + List<Cookie> cookiesInternal = new ArrayList<>(); + for (Cookie cookie : cookies) { + if (!cookieName.equals(cookie.getName())) { + cookiesInternal.add(cookie); + } + } + return cookiesInternal.toArray(new Cookie[0]); + } + + @Override + public Cookie[] getCookies() { + return cookies; + } + } } diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/i18n/HaDispatchMessages.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/i18n/HaDispatchMessages.java index d597c47..9f72638 100644 --- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/i18n/HaDispatchMessages.java +++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/i18n/HaDispatchMessages.java @@ -41,4 +41,7 @@ public interface HaDispatchMessages { @Message(level = MessageLevel.INFO, text = "Error occurred while trying to sleep for failover : {0} {1}") void failoverSleepFailed(String service, @StackTrace(level = MessageLevel.DEBUG) Exception e); + + @Message(level = MessageLevel.ERROR, text = "noFallback flag is turned on for sticky session so aborting request without retrying") + void noFallbackError(); } diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/HaProvider.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/HaProvider.java index 6df79cb..ff543f6 100644 --- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/HaProvider.java +++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/HaProvider.java @@ -64,4 +64,19 @@ public interface HaProvider { */ void markFailedURL(String serviceName, String url); + /** + * This method puts changes the active URL to + * the next available URL for the service. + * + * @param serviceName the name of the service + */ + void makeNextActiveURLAvailable(String serviceName); + + /** + * This method puts gets all the currently + * available URLs for the service. + * + * @param serviceName the name of the service + */ + List<String> getURLs(String serviceName); } diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/HaServiceConfig.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/HaServiceConfig.java index dacb8c6..f0582b4 100644 --- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/HaServiceConfig.java +++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/HaServiceConfig.java @@ -41,4 +41,20 @@ public interface HaServiceConfig { String getZookeeperNamespace(); void setZookeeperNamespace(String zookeeperNamespace); + + boolean isLoadBalancingEnabled(); + + void setLoadBalancingEnabled(boolean isLoadBalancingEnabled); + + boolean isStickySessionEnabled(); + + void setStickySessionEnabled(boolean stickySessionEnabled); + + String getStickySessionCookieName(); + + void setStickySessionCookieName(String stickySessionCookieName); + + boolean isNoFallbackEnabled(); + + void setNoFallbackEnabled(boolean noFallbackEnabled); } diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/URLManager.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/URLManager.java index 7f54c56..383cd8d 100644 --- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/URLManager.java +++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/URLManager.java @@ -33,4 +33,6 @@ public interface URLManager { void setURLs(List<String> urls); void markFailed(String url); + + void makeNextActiveURLAvailable(); } diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/BaseZookeeperURLManager.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/BaseZookeeperURLManager.java index 65f42ec..d3e61ab 100644 --- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/BaseZookeeperURLManager.java +++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/BaseZookeeperURLManager.java @@ -57,9 +57,10 @@ public abstract class BaseZookeeperURLManager implements URLManager { */ private static final int TIMEOUT = 5000; + private final ConcurrentLinkedQueue<String> urls = new ConcurrentLinkedQueue<>(); + private String zooKeeperEnsemble; private String zooKeeperNamespace; - private ConcurrentLinkedQueue<String> urls = new ConcurrentLinkedQueue<>(); // ------------------------------------------------------------------------------------- // URLManager interface methods @@ -115,6 +116,12 @@ public abstract class BaseZookeeperURLManager implements URLManager { } @Override + public void makeNextActiveURLAvailable() { + String head = urls.poll(); + urls.offer(head); + } + + @Override public synchronized void setURLs(List<String> urls) { if ((urls != null) && (!(urls.isEmpty()))) { this.urls.clear(); diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/DefaultHaProvider.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/DefaultHaProvider.java index adc3606..8477e83 100644 --- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/DefaultHaProvider.java +++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/DefaultHaProvider.java @@ -25,6 +25,7 @@ import org.apache.knox.gateway.ha.provider.URLManagerLoader; import org.apache.knox.gateway.ha.provider.impl.i18n.HaMessages; import org.apache.knox.gateway.i18n.messages.MessagesFactory; +import java.util.Collections; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @@ -60,10 +61,7 @@ public class DefaultHaProvider implements HaProvider { @Override public boolean isHaEnabled(String serviceName) { HaServiceConfig config = descriptor.getServiceConfig(serviceName); - if ( config != null && config.isEnabled() ) { - return true; - } - return false; + return config != null && config.isEnabled(); } @Override @@ -93,4 +91,23 @@ public class DefaultHaProvider implements HaProvider { LOG.noServiceFound(serviceName); } } + + @Override + public void makeNextActiveURLAvailable(String serviceName) { + if ( haServices.containsKey(serviceName) ) { + haServices.get(serviceName).makeNextActiveURLAvailable(); + } else { + LOG.noServiceFound(serviceName); + } + } + + @Override + public List<String> getURLs(String serviceName) { + if ( haServices.containsKey(serviceName) ) { + return haServices.get(serviceName).getURLs(); + } else { + LOG.noServiceFound(serviceName); + return Collections.emptyList(); + } + } } diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/DefaultHaServiceConfig.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/DefaultHaServiceConfig.java index 2bf6bf3..3a8a654 100644 --- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/DefaultHaServiceConfig.java +++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/DefaultHaServiceConfig.java @@ -29,6 +29,14 @@ public class DefaultHaServiceConfig implements HaServiceConfig, HaServiceConfigC private int failoverSleep = DEFAULT_FAILOVER_SLEEP; + private boolean isStickySessionEnabled = DEFAULT_STICKY_SESSIONS_ENABLED; + + private boolean isLoadBalancingEnabled = DEFAULT_LOAD_BALANCING_ENABLED; + + private boolean isNoFallbackEnabled = DEFAULT_NO_FALLBACK_ENABLED; + + private String stickySessionCookieName = DEFAULT_STICKY_SESSION_COOKIE_NAME; + private String zookeeperEnsemble; private String zookeeperNamespace; @@ -96,4 +104,44 @@ public class DefaultHaServiceConfig implements HaServiceConfig, HaServiceConfigC public void setZookeeperNamespace(String zookeeperNamespace) { this.zookeeperNamespace = zookeeperNamespace; } + + @Override + public boolean isStickySessionEnabled() { + return this.isStickySessionEnabled; + } + + @Override + public void setStickySessionEnabled(boolean stickySessionEnabled) { + this.isStickySessionEnabled = stickySessionEnabled; + } + + @Override + public String getStickySessionCookieName() { + return this.stickySessionCookieName; + } + + @Override + public void setStickySessionCookieName(String stickySessionCookieName) { + this.stickySessionCookieName = stickySessionCookieName; + } + + @Override + public boolean isLoadBalancingEnabled() { + return this.isLoadBalancingEnabled; + } + + @Override + public void setLoadBalancingEnabled(boolean isLoadBalancingEnabled) { + this.isLoadBalancingEnabled = isLoadBalancingEnabled; + } + + @Override + public boolean isNoFallbackEnabled() { + return isNoFallbackEnabled; + } + + @Override + public void setNoFallbackEnabled(boolean noFallbackEnabled) { + isNoFallbackEnabled = noFallbackEnabled; + } } diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/DefaultURLManager.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/DefaultURLManager.java index 6f909c2..34b2cc8 100644 --- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/DefaultURLManager.java +++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/DefaultURLManager.java @@ -31,8 +31,7 @@ public class DefaultURLManager implements URLManager { private static final HaMessages LOG = MessagesFactory.get(HaMessages.class); - private ConcurrentLinkedQueue<String> urls = new ConcurrentLinkedQueue<>(); - + private final ConcurrentLinkedQueue<String> urls = new ConcurrentLinkedQueue<>(); @Override public boolean supportsConfig(HaServiceConfig config) { @@ -97,4 +96,10 @@ public class DefaultURLManager implements URLManager { } } } + + @Override + public synchronized void makeNextActiveURLAvailable() { + String head = urls.poll(); + urls.offer(head); + } } diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorConstants.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorConstants.java index a23f732..5025863 100644 --- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorConstants.java +++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorConstants.java @@ -43,4 +43,12 @@ public interface HaDescriptorConstants { String ZOOKEEPER_ENSEMBLE = "zookeeperEnsemble"; String ZOOKEEPER_NAMESPACE = "zookeeperNamespace"; + + String ENABLE_LOAD_BALANCING = "enableLoadBalancing"; + + String ENABLE_STICKY_SESSIONS = "enableStickySession"; + + String ENABLE_NO_FALLBACK = "noFallback"; + + String STICKY_SESSION_COOKIE_NAME = "stickySessionCookieName"; } diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorFactory.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorFactory.java index c8ff28e..411a3d6 100644 --- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorFactory.java +++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorFactory.java @@ -36,16 +36,27 @@ public abstract class HaDescriptorFactory implements HaServiceConfigConstants { String failoverSleep = configMap.get(CONFIG_PARAM_FAILOVER_SLEEP); String zookeeperEnsemble = configMap.get(CONFIG_PARAM_ZOOKEEPER_ENSEMBLE); String zookeeperNamespace = configMap.get(CONFIG_PARAM_ZOOKEEPER_NAMESPACE); + String stickySessionEnabled = configMap.get(CONFIG_STICKY_SESSIONS_ENABLED); + String loadBalancingEnabled = configMap.get(CONFIG_LOAD_BALANCING_ENABLED); + String stickySessionCookieName = configMap.get(STICKY_SESSION_COOKIE_NAME); + String noFallbackEnabled = configMap.get(CONFIG_NO_FALLBACK_ENABLED); return createServiceConfig(serviceName, enabledValue, maxFailoverAttempts, failoverSleep, - zookeeperEnsemble, zookeeperNamespace); + zookeeperEnsemble, zookeeperNamespace, loadBalancingEnabled, stickySessionEnabled, stickySessionCookieName, noFallbackEnabled); } public static HaServiceConfig createServiceConfig(String serviceName, String enabledValue, String maxFailoverAttemptsValue, String failoverSleepValue, - String zookeeperEnsemble, String zookeeperNamespace) { + String zookeeperEnsemble, String zookeeperNamespace, + String loadBalancingEnabledValue, String stickySessionsEnabledValue, + String stickySessionCookieNameValue, + String noFallbackEnabledValue) { boolean enabled = DEFAULT_ENABLED; int maxFailoverAttempts = DEFAULT_MAX_FAILOVER_ATTEMPTS; int failoverSleep = DEFAULT_FAILOVER_SLEEP; + boolean stickySessionsEnabled = DEFAULT_STICKY_SESSIONS_ENABLED; + boolean loadBalancingEnabled = DEFAULT_LOAD_BALANCING_ENABLED; + boolean noFallbackEnabled = DEFAULT_NO_FALLBACK_ENABLED; + String stickySessionCookieName = DEFAULT_STICKY_SESSION_COOKIE_NAME; if (enabledValue != null && !enabledValue.trim().isEmpty()) { enabled = Boolean.parseBoolean(enabledValue); } @@ -55,6 +66,18 @@ public abstract class HaDescriptorFactory implements HaServiceConfigConstants { if (failoverSleepValue != null && !failoverSleepValue.trim().isEmpty()) { failoverSleep = Integer.parseInt(failoverSleepValue); } + if (stickySessionsEnabledValue != null && !stickySessionsEnabledValue.trim().isEmpty()) { + stickySessionsEnabled = Boolean.parseBoolean(stickySessionsEnabledValue); + } + if (loadBalancingEnabledValue != null && !loadBalancingEnabledValue.trim().isEmpty()) { + loadBalancingEnabled = Boolean.parseBoolean(loadBalancingEnabledValue); + } + if (stickySessionCookieNameValue != null && !stickySessionCookieNameValue.trim().isEmpty()) { + stickySessionCookieName = stickySessionCookieNameValue; + } + if (noFallbackEnabledValue != null && !noFallbackEnabledValue.trim().isEmpty()) { + noFallbackEnabled = Boolean.parseBoolean(noFallbackEnabledValue); + } DefaultHaServiceConfig serviceConfig = new DefaultHaServiceConfig(serviceName); serviceConfig.setEnabled(enabled); @@ -62,6 +85,10 @@ public abstract class HaDescriptorFactory implements HaServiceConfigConstants { serviceConfig.setFailoverSleep(failoverSleep); serviceConfig.setZookeeperEnsemble(zookeeperEnsemble); serviceConfig.setZookeeperNamespace(zookeeperNamespace); + serviceConfig.setStickySessionEnabled(stickySessionsEnabled); + serviceConfig.setLoadBalancingEnabled(loadBalancingEnabled); + serviceConfig.setStickySessionCookieName(stickySessionCookieName); + serviceConfig.setNoFallbackEnabled(noFallbackEnabled); return serviceConfig; } diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorManager.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorManager.java index bf52a48..4b4487d 100644 --- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorManager.java +++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorManager.java @@ -60,6 +60,12 @@ public class HaDescriptorManager implements HaDescriptorConstants { if (config.getZookeeperNamespace() != null) { serviceElement.setAttribute(ZOOKEEPER_NAMESPACE, config.getZookeeperNamespace()); } + serviceElement.setAttribute(ENABLE_LOAD_BALANCING, Boolean.toString(config.isLoadBalancingEnabled())); + serviceElement.setAttribute(ENABLE_STICKY_SESSIONS, Boolean.toString(config.isStickySessionEnabled())); + serviceElement.setAttribute(ENABLE_NO_FALLBACK, Boolean.toString(config.isNoFallbackEnabled())); + if (config.getStickySessionCookieName() != null) { + serviceElement.setAttribute(STICKY_SESSION_COOKIE_NAME, config.getStickySessionCookieName()); + } root.appendChild(serviceElement); } } @@ -85,7 +91,11 @@ public class HaDescriptorManager implements HaDescriptorConstants { element.getAttribute(MAX_FAILOVER_ATTEMPTS), element.getAttribute(FAILOVER_SLEEP), element.getAttribute(ZOOKEEPER_ENSEMBLE), - element.getAttribute(ZOOKEEPER_NAMESPACE)); + element.getAttribute(ZOOKEEPER_NAMESPACE), + element.getAttribute(ENABLE_LOAD_BALANCING), + element.getAttribute(ENABLE_STICKY_SESSIONS), + element.getAttribute(STICKY_SESSION_COOKIE_NAME), + element.getAttribute(ENABLE_NO_FALLBACK)); descriptor.addServiceConfig(config); } } diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaServiceConfigConstants.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaServiceConfigConstants.java index a8c14fb..52d181c 100644 --- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaServiceConfigConstants.java +++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaServiceConfigConstants.java @@ -32,9 +32,25 @@ public interface HaServiceConfigConstants { String CONFIG_PARAM_ZOOKEEPER_NAMESPACE = "zookeeperNamespace"; + String CONFIG_STICKY_SESSIONS_ENABLED = "enableStickySession"; + + String CONFIG_LOAD_BALANCING_ENABLED = "enableLoadBalancing"; + + String CONFIG_NO_FALLBACK_ENABLED = "noFallback"; + + String STICKY_SESSION_COOKIE_NAME = "stickySessionCookieName"; + int DEFAULT_MAX_FAILOVER_ATTEMPTS = 3; int DEFAULT_FAILOVER_SLEEP = 1000; boolean DEFAULT_ENABLED = true; + + boolean DEFAULT_STICKY_SESSIONS_ENABLED = false; + + boolean DEFAULT_LOAD_BALANCING_ENABLED = false; + + boolean DEFAULT_NO_FALLBACK_ENABLED = false; + + String DEFAULT_STICKY_SESSION_COOKIE_NAME = "KNOX_BACKEND"; } diff --git a/gateway-provider-ha/src/test/java/org/apache/knox/gateway/ha/dispatch/DefaultHaDispatchTest.java b/gateway-provider-ha/src/test/java/org/apache/knox/gateway/ha/dispatch/DefaultHaDispatchTest.java index d0b5492..dc9bcf2 100644 --- a/gateway-provider-ha/src/test/java/org/apache/knox/gateway/ha/dispatch/DefaultHaDispatchTest.java +++ b/gateway-provider-ha/src/test/java/org/apache/knox/gateway/ha/dispatch/DefaultHaDispatchTest.java @@ -17,6 +17,7 @@ */ package org.apache.knox.gateway.ha.dispatch; +import org.apache.http.impl.client.HttpClients; import org.apache.knox.gateway.ha.provider.HaDescriptor; import org.apache.knox.gateway.ha.provider.HaProvider; import org.apache.knox.gateway.ha.provider.HaServletContextListener; @@ -48,7 +49,7 @@ public class DefaultHaDispatchTest { public void testConnectivityFailover() throws Exception { String serviceName = "OOZIE"; HaDescriptor descriptor = HaDescriptorFactory.createDescriptor(); - descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig(serviceName, "true", "1", "1000", null, null)); + descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig(serviceName, "true", "1", "1000", null, null, null, null, null, null)); HaProvider provider = new DefaultHaProvider(descriptor); URI uri1 = new URI( "http://unreachable-host.invalid" ); URI uri2 = new URI( "http://reachable-host.invalid" ); @@ -106,4 +107,184 @@ public class DefaultHaDispatchTest { //test to make sure the sleep took place Assert.assertTrue(elapsedTime > 1000); } + + @Test + public void testStickyFailoverNoFallback() throws Exception { + String serviceName = "OOZIE"; + HaDescriptor descriptor = HaDescriptorFactory.createDescriptor(); + descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig(serviceName, "true", "1", "1000", null, null, null, "true", null, "true")); + HaProvider provider = new DefaultHaProvider(descriptor); + URI uri1 = new URI( "http://unreachable-host.invalid" ); + URI uri2 = new URI( "http://reachable-host.invalid" ); + ArrayList<String> urlList = new ArrayList<>(); + urlList.add(uri1.toString()); + urlList.add(uri2.toString()); + provider.addHaService(serviceName, urlList); + FilterConfig filterConfig = EasyMock.createNiceMock(FilterConfig.class); + ServletContext servletContext = EasyMock.createNiceMock(ServletContext.class); + + EasyMock.expect(filterConfig.getServletContext()).andReturn(servletContext).anyTimes(); + EasyMock.expect(servletContext.getAttribute(HaServletContextListener.PROVIDER_ATTRIBUTE_NAME)).andReturn(provider).anyTimes(); + + BasicHttpParams params = new BasicHttpParams(); + + HttpUriRequest outboundRequest = EasyMock.createNiceMock(HttpRequestBase.class); + EasyMock.expect(outboundRequest.getMethod()).andReturn( "GET" ).anyTimes(); + EasyMock.expect(outboundRequest.getURI()).andReturn( uri1 ).anyTimes(); + EasyMock.expect(outboundRequest.getParams()).andReturn( params ).anyTimes(); + + HttpServletRequest inboundRequest = EasyMock.createNiceMock(HttpServletRequest.class); + EasyMock.expect(inboundRequest.getRequestURL()).andReturn( new StringBuffer(uri2.toString()) ).once(); + EasyMock.expect(inboundRequest.getAttribute("dispatch.ha.failover.counter")).andReturn(new AtomicInteger(0)).once(); + EasyMock.expect(inboundRequest.getAttribute("dispatch.ha.failover.counter")).andReturn(new AtomicInteger(1)).once(); + + HttpServletResponse outboundResponse = EasyMock.createNiceMock(HttpServletResponse.class); + EasyMock.expect(outboundResponse.getOutputStream()).andAnswer( new IAnswer<SynchronousServletOutputStreamAdapter>() { + @Override + public SynchronousServletOutputStreamAdapter answer() { + return new SynchronousServletOutputStreamAdapter() { + @Override + public void write( int b ) throws IOException { + throw new IOException( "unreachable-host.invalid" ); + } + }; + } + }).once(); + EasyMock.replay(filterConfig, servletContext, outboundRequest, inboundRequest, outboundResponse); + Assert.assertEquals(uri1.toString(), provider.getActiveURL(serviceName)); + DefaultHaDispatch dispatch = new DefaultHaDispatch(); + HttpClientBuilder builder = HttpClientBuilder.create(); + CloseableHttpClient client = builder.build(); + dispatch.setHttpClient(client); + dispatch.setHaProvider(provider); + dispatch.setServiceRole(serviceName); + dispatch.init(); + try { + dispatch.executeRequest(outboundRequest, inboundRequest, outboundResponse); + } catch (IOException e) { + //this is expected after the failover limit is reached + } + /* since fallback did not happen */ + Assert.assertNotEquals(uri2.toString(), provider.getActiveURL(serviceName)); + } + + /** + * This is a negative test for noFallback flag + * When sticky session is disabled noFallback should not have any effect + * i.e. request should failover. + * @throws Exception + */ + @Test + public void testNoFallbackWhenStickyDisabled() throws Exception { + String serviceName = "OOZIE"; + HaDescriptor descriptor = HaDescriptorFactory.createDescriptor(); + descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig(serviceName, "true", "1", "1000", null, null, null, null, null, "true")); + HaProvider provider = new DefaultHaProvider(descriptor); + URI uri1 = new URI( "http://unreachable-host.invalid" ); + URI uri2 = new URI( "http://reachable-host.invalid" ); + ArrayList<String> urlList = new ArrayList<>(); + urlList.add(uri1.toString()); + urlList.add(uri2.toString()); + provider.addHaService(serviceName, urlList); + FilterConfig filterConfig = EasyMock.createNiceMock(FilterConfig.class); + ServletContext servletContext = EasyMock.createNiceMock(ServletContext.class); + + EasyMock.expect(filterConfig.getServletContext()).andReturn(servletContext).anyTimes(); + EasyMock.expect(servletContext.getAttribute(HaServletContextListener.PROVIDER_ATTRIBUTE_NAME)).andReturn(provider).anyTimes(); + + BasicHttpParams params = new BasicHttpParams(); + + HttpUriRequest outboundRequest = EasyMock.createNiceMock(HttpRequestBase.class); + EasyMock.expect(outboundRequest.getMethod()).andReturn( "GET" ).anyTimes(); + EasyMock.expect(outboundRequest.getURI()).andReturn( uri1 ).anyTimes(); + EasyMock.expect(outboundRequest.getParams()).andReturn( params ).anyTimes(); + + HttpServletRequest inboundRequest = EasyMock.createNiceMock(HttpServletRequest.class); + EasyMock.expect(inboundRequest.getRequestURL()).andReturn( new StringBuffer(uri2.toString()) ).once(); + EasyMock.expect(inboundRequest.getAttribute("dispatch.ha.failover.counter")).andReturn(new AtomicInteger(0)).once(); + EasyMock.expect(inboundRequest.getAttribute("dispatch.ha.failover.counter")).andReturn(new AtomicInteger(1)).once(); + + HttpServletResponse outboundResponse = EasyMock.createNiceMock(HttpServletResponse.class); + EasyMock.expect(outboundResponse.getOutputStream()).andAnswer( new IAnswer<SynchronousServletOutputStreamAdapter>() { + @Override + public SynchronousServletOutputStreamAdapter answer() { + return new SynchronousServletOutputStreamAdapter() { + @Override + public void write( int b ) throws IOException { + throw new IOException( "unreachable-host.invalid" ); + } + }; + } + }).once(); + EasyMock.replay(filterConfig, servletContext, outboundRequest, inboundRequest, outboundResponse); + Assert.assertEquals(uri1.toString(), provider.getActiveURL(serviceName)); + DefaultHaDispatch dispatch = new DefaultHaDispatch(); + HttpClientBuilder builder = HttpClientBuilder.create(); + CloseableHttpClient client = builder.build(); + dispatch.setHttpClient(client); + dispatch.setHaProvider(provider); + dispatch.setServiceRole(serviceName); + dispatch.init(); + long startTime = System.currentTimeMillis(); + try { + dispatch.executeRequest(outboundRequest, inboundRequest, outboundResponse); + } catch (IOException e) { + //this is expected after the failover limit is reached + } + long elapsedTime = System.currentTimeMillis() - startTime; + Assert.assertEquals(uri2.toString(), provider.getActiveURL(serviceName)); + //test to make sure the sleep took place + Assert.assertTrue(elapsedTime > 1000); + } + + @Test + public void testConnectivityActive() throws Exception { + String serviceName = "OOZIE"; + HaDescriptor descriptor = HaDescriptorFactory.createDescriptor(); + descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig(serviceName, "true", "1", "1000", null, null, null, "true", null, null)); + HaProvider provider = new DefaultHaProvider(descriptor); + URI uri1 = new URI( "http://unreachable-host" ); + URI uri2 = new URI( "http://reachable-host" ); + ArrayList<String> urlList = new ArrayList<>(); + urlList.add(uri1.toString()); + urlList.add(uri2.toString()); + provider.addHaService(serviceName, urlList); + FilterConfig filterConfig = EasyMock.createNiceMock(FilterConfig.class); + ServletContext servletContext = EasyMock.createNiceMock(ServletContext.class); + + EasyMock.expect(filterConfig.getServletContext()).andReturn(servletContext).anyTimes(); + EasyMock.expect(servletContext.getAttribute(HaServletContextListener.PROVIDER_ATTRIBUTE_NAME)).andReturn(provider).anyTimes(); + + BasicHttpParams params = new BasicHttpParams(); + + HttpUriRequest outboundRequest = EasyMock.createNiceMock(HttpRequestBase.class); + EasyMock.expect(outboundRequest.getMethod()).andReturn( "GET" ).anyTimes(); + EasyMock.expect(outboundRequest.getURI()).andReturn( uri1 ).anyTimes(); + EasyMock.expect(outboundRequest.getParams()).andReturn( params ).anyTimes(); + + HttpServletRequest inboundRequest = EasyMock.createNiceMock(HttpServletRequest.class); + EasyMock.expect(inboundRequest.getRequestURL()).andReturn( new StringBuffer(uri2.toString()) ).once(); + EasyMock.expect(inboundRequest.getAttribute("dispatch.ha.failover.counter")).andReturn(new AtomicInteger(0)).once(); + EasyMock.expect(inboundRequest.getAttribute("dispatch.ha.failover.counter")).andReturn(new AtomicInteger(1)).once(); + + HttpServletResponse outboundResponse = EasyMock.createNiceMock(HttpServletResponse.class); + EasyMock.expect(outboundResponse.getOutputStream()).andAnswer( new IAnswer<SynchronousServletOutputStreamAdapter>() { + @Override + public SynchronousServletOutputStreamAdapter answer() { + return new SynchronousServletOutputStreamAdapter() { + @Override + public void write( int b ) throws IOException { + throw new IOException( "unreachable-host" ); + } + }; + } + }).once(); + EasyMock.replay(filterConfig, servletContext, outboundRequest, inboundRequest, outboundResponse); + Assert.assertEquals(uri1.toString(), provider.getActiveURL(serviceName)); + DefaultHaDispatch dispatch = new DefaultHaDispatch(); + dispatch.setHttpClient(HttpClients.createDefault()); + dispatch.setHaProvider(provider); + dispatch.setServiceRole(serviceName); + dispatch.init(); + } } diff --git a/gateway-provider-ha/src/test/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorFactoryTest.java b/gateway-provider-ha/src/test/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorFactoryTest.java index ea2f62c..6a95711 100644 --- a/gateway-provider-ha/src/test/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorFactoryTest.java +++ b/gateway-provider-ha/src/test/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorFactoryTest.java @@ -41,12 +41,70 @@ public class HaDescriptorFactoryTest { assertEquals(42, serviceConfig.getMaxFailoverAttempts()); assertEquals(50, serviceConfig.getFailoverSleep()); - serviceConfig = HaDescriptorFactory.createServiceConfig("bar", "false", "3", "1000", null, null); + serviceConfig = HaDescriptorFactory.createServiceConfig("bar", "false", "3", "1000", null, null, null, null, null, null); assertNotNull(serviceConfig); assertFalse(serviceConfig.isEnabled()); assertEquals("bar", serviceConfig.getServiceName()); assertEquals(3, serviceConfig.getMaxFailoverAttempts()); assertEquals(1000, serviceConfig.getFailoverSleep()); - } + + @Test + public void testCreateServiceConfigActive() { + HaServiceConfig serviceConfig = HaDescriptorFactory.createServiceConfig("foo", "enableStickySession=true;enabled=true;maxFailoverAttempts=42;failoverSleep=50;maxRetryAttempts=1;retrySleep=1000"); + assertNotNull(serviceConfig); + assertTrue(serviceConfig.isEnabled()); + assertEquals("foo", serviceConfig.getServiceName()); + assertEquals(42, serviceConfig.getMaxFailoverAttempts()); + assertEquals(50, serviceConfig.getFailoverSleep()); + assertTrue(serviceConfig.isStickySessionEnabled()); + assertEquals(HaServiceConfigConstants.DEFAULT_STICKY_SESSION_COOKIE_NAME, serviceConfig.getStickySessionCookieName()); + + serviceConfig = HaDescriptorFactory.createServiceConfig("foo", "enableStickySession=true;enabled=true;maxFailoverAttempts=42;failoverSleep=50;maxRetryAttempts=1;retrySleep=1000;stickySessionCookieName=abc"); + assertNotNull(serviceConfig); + assertTrue(serviceConfig.isEnabled()); + assertEquals("foo", serviceConfig.getServiceName()); + assertEquals(42, serviceConfig.getMaxFailoverAttempts()); + assertEquals(50, serviceConfig.getFailoverSleep()); + assertTrue(serviceConfig.isStickySessionEnabled()); + assertEquals("abc", serviceConfig.getStickySessionCookieName()); + + serviceConfig = HaDescriptorFactory.createServiceConfig( "bar", "false", "3", "1000", null, null, null, "true", null, null); + assertNotNull(serviceConfig); + assertFalse(serviceConfig.isEnabled()); + assertEquals("bar", serviceConfig.getServiceName()); + assertEquals(3, serviceConfig.getMaxFailoverAttempts()); + assertEquals(1000, serviceConfig.getFailoverSleep()); + assertTrue(serviceConfig.isStickySessionEnabled()); + assertEquals(HaServiceConfigConstants.DEFAULT_STICKY_SESSION_COOKIE_NAME, serviceConfig.getStickySessionCookieName()); + + serviceConfig = HaDescriptorFactory.createServiceConfig( "knox", "false", "4", "3000", null, null, null, null, null, null); + assertNotNull(serviceConfig); + assertFalse(serviceConfig.isEnabled()); + assertEquals("knox", serviceConfig.getServiceName()); + assertEquals(4, serviceConfig.getMaxFailoverAttempts()); + assertEquals(3000, serviceConfig.getFailoverSleep()); + assertFalse(serviceConfig.isStickySessionEnabled()); + assertEquals(HaServiceConfigConstants.DEFAULT_STICKY_SESSION_COOKIE_NAME, serviceConfig.getStickySessionCookieName()); + + serviceConfig = HaDescriptorFactory.createServiceConfig( "bar", "false", "3", "1000", null, null, null, "true", "abc", null); + assertNotNull(serviceConfig); + assertFalse(serviceConfig.isEnabled()); + assertEquals("bar", serviceConfig.getServiceName()); + assertEquals(3, serviceConfig.getMaxFailoverAttempts()); + assertEquals(1000, serviceConfig.getFailoverSleep()); + assertTrue(serviceConfig.isStickySessionEnabled()); + assertEquals("abc", serviceConfig.getStickySessionCookieName()); + + serviceConfig = HaDescriptorFactory.createServiceConfig( "bar", "false", "3", "1000", null, null, "true", null, "abc", "true"); + assertNotNull(serviceConfig); + assertFalse(serviceConfig.isEnabled()); + assertEquals("bar", serviceConfig.getServiceName()); + assertEquals(3, serviceConfig.getMaxFailoverAttempts()); + assertEquals(1000, serviceConfig.getFailoverSleep()); + assertFalse(serviceConfig.isStickySessionEnabled()); + assertTrue(serviceConfig.isLoadBalancingEnabled()); + assertTrue(serviceConfig.isNoFallbackEnabled()); + assertEquals("abc", serviceConfig.getStickySessionCookieName()); + } } diff --git a/gateway-provider-ha/src/test/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorManagerTest.java b/gateway-provider-ha/src/test/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorManagerTest.java index 5aaf3c7..5f5e36c 100644 --- a/gateway-provider-ha/src/test/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorManagerTest.java +++ b/gateway-provider-ha/src/test/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorManagerTest.java @@ -49,6 +49,8 @@ public class HaDescriptorManagerTest { assertEquals(42, config.getMaxFailoverAttempts()); assertEquals(4000, config.getFailoverSleep()); assertFalse(config.isEnabled()); + assertFalse(config.isStickySessionEnabled()); + assertFalse(config.isNoFallbackEnabled()); config = descriptor.getServiceConfig("bar"); assertTrue(config.isEnabled()); } @@ -66,21 +68,51 @@ public class HaDescriptorManagerTest { assertEquals(HaServiceConfigConstants.DEFAULT_MAX_FAILOVER_ATTEMPTS, config.getMaxFailoverAttempts()); assertEquals(HaServiceConfigConstants.DEFAULT_FAILOVER_SLEEP, config.getFailoverSleep()); assertEquals(HaServiceConfigConstants.DEFAULT_ENABLED, config.isEnabled()); + assertEquals(HaServiceConfigConstants.DEFAULT_LOAD_BALANCING_ENABLED, config.isLoadBalancingEnabled()); + assertEquals(HaServiceConfigConstants.DEFAULT_STICKY_SESSIONS_ENABLED, config.isStickySessionEnabled()); + assertEquals(HaServiceConfigConstants.DEFAULT_NO_FALLBACK_ENABLED, config.isNoFallbackEnabled()); } @Test public void testDescriptorStoring() throws IOException { HaDescriptor descriptor = HaDescriptorFactory.createDescriptor(); - descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig("foo", "false", "42", "1000", "foo:2181,bar:2181", "hiveserver2")); - descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig("bar", "true", "3", "5000", null, null)); + descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig("foo", "false", "42", "1000", "foo:2181,bar:2181", "hiveserver2", null, null, null, null)); + descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig("bar", "true", "3", "5000", null, null, null, null, null, null)); StringWriter writer = new StringWriter(); HaDescriptorManager.store(descriptor, writer); - String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" + - "<ha>\n" + - " <service enabled=\"false\" failoverSleep=\"1000\" maxFailoverAttempts=\"42\" name=\"foo\" zookeeperEnsemble=\"foo:2181,bar:2181\" zookeeperNamespace=\"hiveserver2\"/>\n" + - " <service enabled=\"true\" failoverSleep=\"5000\" maxFailoverAttempts=\"3\" name=\"bar\"/>\n" + - "</ha>\n"; - assertThat( the( xml ), hasXPath( "/ha/service[@enabled='false' and @failoverSleep='1000' and @maxFailoverAttempts='42' and @name='foo' and @zookeeperEnsemble='foo:2181,bar:2181' and @zookeeperNamespace='hiveserver2']" ) ); - assertThat( the( xml ), hasXPath( "/ha/service[@enabled='true' and @failoverSleep='5000' and @maxFailoverAttempts='3' and @name='bar']" ) ); + String xml = writer.toString(); + assertThat( the( xml ), hasXPath( "/ha//service[@enabled='false' and @failoverSleep='1000' and @maxFailoverAttempts='42' and @name='foo' and @zookeeperEnsemble='foo:2181,bar:2181' and @zookeeperNamespace='hiveserver2']" ) ); + assertThat( the( xml ), hasXPath( "/ha//service[@enabled='true' and @failoverSleep='5000' and @maxFailoverAttempts='3' and @name='bar']" ) ); } + + @Test + public void testDescriptorStoringStickySessionCookie() throws IOException { + HaDescriptor descriptor = HaDescriptorFactory.createDescriptor(); + descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig("foo", "false", "42", "1000", "foo:2181,bar:2181", "hiveserver2", null, "true", null, null)); + descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig("bar", "true", "3", "5000", null, null, null, "true", null, null)); + descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig("abc", "true", "3", "5000", null, null, null, "true", "abc", null)); + descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig("xyz", "true", "3", "5000", null, null, null, "true", "xyz", "true")); + + StringWriter writer = new StringWriter(); + HaDescriptorManager.store(descriptor, writer); + String xml = writer.toString(); + assertThat( the( xml ), hasXPath( "/ha//service[@enabled='false' and @failoverSleep='1000' and @maxFailoverAttempts='42' and @name='foo' and @zookeeperEnsemble='foo:2181,bar:2181' and @zookeeperNamespace='hiveserver2' and @enableStickySession='true']" ) ); + assertThat( the( xml ), hasXPath( "/ha//service[@enabled='true' and @failoverSleep='5000' and @maxFailoverAttempts='3' and @name='bar' and @enableStickySession='true']" ) ); + assertThat( the( xml ), hasXPath( "/ha//service[@enabled='true' and @failoverSleep='5000' and @maxFailoverAttempts='3' and @name='abc' and @enableStickySession='true' and @stickySessionCookieName='abc']" ) ); + assertThat( the( xml ), hasXPath( "/ha//service[@enabled='true' and @failoverSleep='5000' and @maxFailoverAttempts='3' and @name='xyz' and @enableStickySession='true' and @stickySessionCookieName='xyz' and @noFallback='true']" ) ); + } + + @Test + public void testDescriptorStoringLoadBalancerConfig() throws IOException { + HaDescriptor descriptor = HaDescriptorFactory.createDescriptor(); + descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig("foo", "false", "42", "1000", "foo:2181,bar:2181", "hiveserver2", "true", "false", null, null)); + descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig("bar", "true", "3", "5000", null, null, "true", null, null, null)); + descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig("abc", "true", "3", "5000", null, null, null, "true", "abc", null)); + StringWriter writer = new StringWriter(); + HaDescriptorManager.store(descriptor, writer); + String xml = writer.toString(); + assertThat( the( xml ), hasXPath( "/ha//service[@enabled='false' and @failoverSleep='1000' and @maxFailoverAttempts='42' and @name='foo' and @zookeeperEnsemble='foo:2181,bar:2181' and @zookeeperNamespace='hiveserver2' and @enableLoadBalancing='true' and @enableStickySession='false']" ) ); + assertThat( the( xml ), hasXPath( "/ha//service[@enabled='true' and @failoverSleep='5000' and @maxFailoverAttempts='3' and @name='bar' and @enableLoadBalancing='true' and @enableStickySession='false']" ) ); + assertThat( the( xml ), hasXPath( "/ha//service[@enabled='true' and @failoverSleep='5000' and @maxFailoverAttempts='3' and @name='abc' and @enableLoadBalancing='false' and @enableStickySession='true' and @stickySessionCookieName='abc']" ) ); + } } diff --git a/gateway-service-rm/src/test/java/org/apache/knox/gateway/rm/dispatch/RMHaDispatchTest.java b/gateway-service-rm/src/test/java/org/apache/knox/gateway/rm/dispatch/RMHaDispatchTest.java index 0c3b1b9..55089d8 100644 --- a/gateway-service-rm/src/test/java/org/apache/knox/gateway/rm/dispatch/RMHaDispatchTest.java +++ b/gateway-service-rm/src/test/java/org/apache/knox/gateway/rm/dispatch/RMHaDispatchTest.java @@ -58,7 +58,7 @@ public class RMHaDispatchTest { public void testConnectivityFailure() throws Exception { String serviceName = "RESOURCEMANAGER"; HaDescriptor descriptor = HaDescriptorFactory.createDescriptor(); - descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig(serviceName, "true", "1", "1000", null, null)); + descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig(serviceName, "true", "1", "1000", null, null, null, null, null, null)); HaProvider provider = new DefaultHaProvider(descriptor); URI uri1 = new URI("http://unreachable-host.invalid"); URI uri2 = new URI("http://reachable-host.invalid"); @@ -129,7 +129,7 @@ public class RMHaDispatchTest { public void testConnectivityFailover() throws Exception { String serviceName = "RESOURCEMANAGER"; HaDescriptor descriptor = HaDescriptorFactory.createDescriptor(); - descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig(serviceName, "true", "1", "1000", null, null)); + descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig(serviceName, "true", "1", "1000", null, null, null, null, null, null)); HaProvider provider = new DefaultHaProvider(descriptor); URI uri1 = new URI("http://passive-host.invalid"); URI uri2 = new URI("http://other-host.invalid"); diff --git a/gateway-service-webhdfs/src/test/java/org/apache/knox/gateway/hdfs/dispatch/WebHdfsHaDispatchTest.java b/gateway-service-webhdfs/src/test/java/org/apache/knox/gateway/hdfs/dispatch/WebHdfsHaDispatchTest.java index 02a4114..518a937 100644 --- a/gateway-service-webhdfs/src/test/java/org/apache/knox/gateway/hdfs/dispatch/WebHdfsHaDispatchTest.java +++ b/gateway-service-webhdfs/src/test/java/org/apache/knox/gateway/hdfs/dispatch/WebHdfsHaDispatchTest.java @@ -48,7 +48,7 @@ public class WebHdfsHaDispatchTest { public void testConnectivityFailover() throws Exception { String serviceName = "WEBHDFS"; HaDescriptor descriptor = HaDescriptorFactory.createDescriptor(); - descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig(serviceName, "true", "1", "1000", null, null)); + descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig(serviceName, "true", "1", "1000", null, null, null, null, null, null)); HaProvider provider = new DefaultHaProvider(descriptor); URI uri1 = new URI( "http://unreachable-host.invalid" ); URI uri2 = new URI( "http://reachable-host.invalid" ); diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/DefaultDispatch.java b/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/DefaultDispatch.java index 0d4608c..304d2c6 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/DefaultDispatch.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/DefaultDispatch.java @@ -24,9 +24,9 @@ import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpHead; import org.apache.http.client.methods.HttpOptions; +import org.apache.http.client.methods.HttpPatch; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; -import org.apache.http.client.methods.HttpPatch; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.entity.ContentType; import org.apache.knox.gateway.SpiGatewayMessages; @@ -116,6 +116,29 @@ public class DefaultDispatch extends AbstractGatewayDispatch { LOG.setReplayBufferSize(replayBufferSize, getServiceRole()); } + /** + * Wrapper around execute request to accommodate any + * request processing such as additional HA logic. + * @param outboundRequest + * @param inboundRequest + * @param outboundResponse + * @throws IOException + */ + protected void executeRequestWrapper(HttpUriRequest outboundRequest, + HttpServletRequest inboundRequest, HttpServletResponse outboundResponse) + throws IOException { + executeRequest(outboundRequest, inboundRequest, outboundResponse); + } + + /** + * A outbound response wrapper used by classes extending this class + * to modify any outgoing + * response i.e. cookies + */ + protected void outboundResponseWrapper(final HttpServletRequest inboundRequest, HttpServletResponse outboundResponse) { + /* no-op */ + } + protected void executeRequest( HttpUriRequest outboundRequest, HttpServletRequest inboundRequest, @@ -159,6 +182,8 @@ public class DefaultDispatch extends AbstractGatewayDispatch { } protected void writeOutboundResponse(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest, HttpServletResponse outboundResponse, HttpResponse inboundResponse) throws IOException { + /* in case any changes to outbound response are needed */ + outboundResponseWrapper(inboundRequest, outboundResponse); // Copy the client respond header to the server respond. outboundResponse.setStatus(inboundResponse.getStatusLine().getStatusCode()); copyResponseHeaderFields(outboundResponse, inboundResponse); @@ -267,14 +292,14 @@ public class DefaultDispatch extends AbstractGatewayDispatch { // and setting params here causes configuration setup there to be ignored there. // method.getParams().setBooleanParameter("http.protocol.handle-redirects", false); copyRequestHeaderFields(method, request); - executeRequest(method, request, response); + executeRequestWrapper(method, request, response); } @Override public void doOptions(URI url, HttpServletRequest request, HttpServletResponse response) throws IOException { HttpOptions method = new HttpOptions(url); - executeRequest(method, request, response); + executeRequestWrapper(method, request, response); } @Override @@ -284,7 +309,7 @@ public class DefaultDispatch extends AbstractGatewayDispatch { HttpEntity entity = createRequestEntity(request); method.setEntity(entity); copyRequestHeaderFields(method, request); - executeRequest(method, request, response); + executeRequestWrapper(method, request, response); } @Override @@ -294,7 +319,7 @@ public class DefaultDispatch extends AbstractGatewayDispatch { HttpEntity entity = createRequestEntity(request); method.setEntity(entity); copyRequestHeaderFields(method, request); - executeRequest(method, request, response); + executeRequestWrapper(method, request, response); } @Override @@ -304,7 +329,7 @@ public class DefaultDispatch extends AbstractGatewayDispatch { HttpEntity entity = createRequestEntity(request); method.setEntity(entity); copyRequestHeaderFields(method, request); - executeRequest(method, request, response); + executeRequestWrapper(method, request, response); } @Override @@ -312,7 +337,7 @@ public class DefaultDispatch extends AbstractGatewayDispatch { throws IOException { HttpDelete method = new HttpDelete(url); copyRequestHeaderFields(method, request); - executeRequest(method, request, response); + executeRequestWrapper(method, request, response); } @Override @@ -320,7 +345,7 @@ public class DefaultDispatch extends AbstractGatewayDispatch { throws IOException { final HttpHead method = new HttpHead(url); copyRequestHeaderFields(method, request); - executeRequest(method, request, response); + executeRequestWrapper(method, request, response); } public void copyResponseHeaderFields(HttpServletResponse outboundResponse, HttpResponse inboundResponse) {