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 48d04f3 KNOX-2545 - Account for loadbalancing and stickysession corner cases. (#417) 48d04f3 is described below commit 48d04f3053e3ab92c2aab6bd4e75cf4086efa154 Author: Sandeep Moré <moresand...@gmail.com> AuthorDate: Wed Mar 17 05:57:30 2021 -0400 KNOX-2545 - Account for loadbalancing and stickysession corner cases. (#417) * KNOX-2545 - Account for loadbalancing and stickysession corner cases. --- .../ha/dispatch/ConfigurableHADispatch.java | 37 +- .../gateway/ha/dispatch/DefaultHaDispatchTest.java | 419 ++++++++++++++++++++- 2 files changed, 437 insertions(+), 19 deletions(-) diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/ConfigurableHADispatch.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/ConfigurableHADispatch.java index 6c98abb..a7849a3 100644 --- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/ConfigurableHADispatch.java +++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/ConfigurableHADispatch.java @@ -68,8 +68,8 @@ public class ConfigurableHADispatch extends ConfigurableDispatch { private static final Map<String, String> urlToHashLookup = new HashMap<>(); private static final Map<String, String> hashToUrlLookup = new HashMap<>(); - private boolean stickySessionsEnabled = HaServiceConfigConstants.DEFAULT_STICKY_SESSIONS_ENABLED; private boolean loadBalancingEnabled = HaServiceConfigConstants.DEFAULT_LOAD_BALANCING_ENABLED; + private boolean stickySessionsEnabled = HaServiceConfigConstants.DEFAULT_STICKY_SESSIONS_ENABLED; private boolean noFallbackEnabled = HaServiceConfigConstants.DEFAULT_NO_FALLBACK_ENABLED; private String stickySessionCookieName = HaServiceConfigConstants.DEFAULT_STICKY_SESSION_COOKIE_NAME; @@ -81,10 +81,14 @@ public class ConfigurableHADispatch extends ConfigurableDispatch { HaServiceConfig serviceConfig = haProvider.getHaDescriptor().getServiceConfig(getServiceRole()); maxFailoverAttempts = serviceConfig.getMaxFailoverAttempts(); failoverSleep = serviceConfig.getFailoverSleep(); - stickySessionsEnabled = serviceConfig.isStickySessionEnabled(); loadBalancingEnabled = serviceConfig.isLoadBalancingEnabled(); - noFallbackEnabled = serviceConfig.isNoFallbackEnabled(); - stickySessionCookieName = serviceConfig.getStickySessionCookieName(); + + /* enforce dependency */ + stickySessionsEnabled = loadBalancingEnabled && serviceConfig.isStickySessionEnabled(); + noFallbackEnabled = stickySessionsEnabled && serviceConfig.isNoFallbackEnabled(); + if(stickySessionsEnabled) { + stickySessionCookieName = serviceConfig.getStickySessionCookieName(); + } setupUrlHashLookup(); } @@ -120,12 +124,25 @@ public class ConfigurableHADispatch extends ConfigurableDispatch { } 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) + * 1. Load balance when loadbalancing is enabled. + * 2. Loadbalance only when sticky session is enabled but cookie not detected + * i.e. when loadbalancing is enabled every request that does not have BACKEND cookie + * needs to be loadbalanced. If a request has BACKEND coookie and Loadbalance=on then + * there should be no loadbalancing. */ - if ( (!backendURI.isPresent() && stickySessionsEnabled) || loadBalancingEnabled) { + if (loadBalancingEnabled) { + /* check sticky session enabled */ + if(stickySessionsEnabled) { + /* loadbalance only when sticky session enabled and no backend url cookie */ + if(!backendURI.isPresent()) { + haProvider.makeNextActiveURLAvailable(getServiceRole()); + } else{ + /* sticky session enabled and backend url cookie is valid no need to loadbalance */ + /* do nothing */ + } + } else { haProvider.makeNextActiveURLAvailable(getServiceRole()); + } } } @@ -148,7 +165,7 @@ public class ConfigurableHADispatch extends ConfigurableDispatch { } private Optional<URI> setBackendfromHaCookie(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest) { - if (stickySessionsEnabled && inboundRequest.getCookies() != null) { + if (loadBalancingEnabled && stickySessionsEnabled && inboundRequest.getCookies() != null) { for (Cookie cookie : inboundRequest.getCookies()) { if (stickySessionCookieName.equals(cookie.getName())) { String backendURLHash = cookie.getValue(); @@ -207,7 +224,7 @@ public class ConfigurableHADispatch extends ConfigurableDispatch { 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) { + if(stickySessionsEnabled && noFallbackEnabled) { LOG.noFallbackError(); outboundResponse.sendError(HttpServletResponse.SC_BAD_GATEWAY, "Service connection error, HA failover disabled"); return; 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 7e51904..2584957 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,7 +17,14 @@ */ package org.apache.knox.gateway.ha.dispatch; +import org.apache.http.Header; +import org.apache.http.HeaderElement; +import org.apache.http.HttpEntity; +import org.apache.http.HttpStatus; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.impl.client.HttpClients; +import org.apache.knox.gateway.config.GatewayConfig; import org.apache.knox.gateway.ha.provider.HaDescriptor; import org.apache.knox.gateway.ha.provider.HaProvider; import org.apache.knox.gateway.ha.provider.HaServletContextListener; @@ -36,10 +43,13 @@ import org.junit.Test; import javax.servlet.FilterConfig; import javax.servlet.ServletContext; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicInteger; @@ -89,7 +99,7 @@ public class DefaultHaDispatchTest { }).once(); EasyMock.replay(filterConfig, servletContext, outboundRequest, inboundRequest, outboundResponse); Assert.assertEquals(uri1.toString(), provider.getActiveURL(serviceName)); - DefaultHaDispatch dispatch = new DefaultHaDispatch(); + ConfigurableHADispatch dispatch = new ConfigurableHADispatch(); HttpClientBuilder builder = HttpClientBuilder.create(); CloseableHttpClient client = builder.build(); dispatch.setHttpClient(client); @@ -98,7 +108,7 @@ public class DefaultHaDispatchTest { dispatch.init(); long startTime = System.currentTimeMillis(); try { - dispatch.executeRequest(outboundRequest, inboundRequest, outboundResponse); + dispatch.executeRequestWrapper(outboundRequest, inboundRequest, outboundResponse); } catch (IOException e) { //this is expected after the failover limit is reached } @@ -108,8 +118,13 @@ public class DefaultHaDispatchTest { Assert.assertTrue(elapsedTime > 1000); } + /** + * Test failover when loadbalancing=false, sticky=true, nofallback=true. + * should failover. + * @throws Exception + */ @Test - public void testStickyFailoverNoFallback() throws Exception { + public void testNoLoadbalancingStickyFailoverNoFallback() throws Exception { String serviceName = "OOZIE"; HaDescriptor descriptor = HaDescriptorFactory.createDescriptor(); descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig(serviceName, "true", "1", "1000", null, null, null, "true", null, "true")); @@ -152,7 +167,7 @@ public class DefaultHaDispatchTest { }).once(); EasyMock.replay(filterConfig, servletContext, outboundRequest, inboundRequest, outboundResponse); Assert.assertEquals(uri1.toString(), provider.getActiveURL(serviceName)); - DefaultHaDispatch dispatch = new DefaultHaDispatch(); + ConfigurableHADispatch dispatch = new ConfigurableHADispatch(); HttpClientBuilder builder = HttpClientBuilder.create(); CloseableHttpClient client = builder.build(); dispatch.setHttpClient(client); @@ -160,12 +175,12 @@ public class DefaultHaDispatchTest { dispatch.setServiceRole(serviceName); dispatch.init(); try { - dispatch.executeRequest(outboundRequest, inboundRequest, outboundResponse); + dispatch.executeRequestWrapper(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)); + /* since fallback happens */ + Assert.assertEquals(uri2.toString(), provider.getActiveURL(serviceName)); } /** @@ -218,7 +233,7 @@ public class DefaultHaDispatchTest { }).once(); EasyMock.replay(filterConfig, servletContext, outboundRequest, inboundRequest, outboundResponse); Assert.assertEquals(uri1.toString(), provider.getActiveURL(serviceName)); - DefaultHaDispatch dispatch = new DefaultHaDispatch(); + ConfigurableHADispatch dispatch = new ConfigurableHADispatch(); HttpClientBuilder builder = HttpClientBuilder.create(); CloseableHttpClient client = builder.build(); dispatch.setHttpClient(client); @@ -227,7 +242,7 @@ public class DefaultHaDispatchTest { dispatch.init(); long startTime = System.currentTimeMillis(); try { - dispatch.executeRequest(outboundRequest, inboundRequest, outboundResponse); + dispatch.executeRequestWrapper(outboundRequest, inboundRequest, outboundResponse); } catch (IOException e) { //this is expected after the failover limit is reached } @@ -237,6 +252,392 @@ public class DefaultHaDispatchTest { Assert.assertTrue(elapsedTime > 1000); } + /** + * Test the case where loadbalancing is off and sticky session is on + * Expected behavior: When loadbalncing is off sticky sessions on is + * that there should be no url loadbalancing + * @throws Exception + */ + @Test + public void testLoadbalancingOffStickyOn() 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://host1.valid" ); + URI uri2 = new URI( "http://host2.valid" ); + 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(); + + /* backend request */ + 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(); + + /* backend response */ + CloseableHttpResponse inboundResponse = EasyMock.createNiceMock(CloseableHttpResponse.class); + final StatusLine statusLine = EasyMock.createNiceMock(StatusLine.class); + final HttpEntity entity = EasyMock.createNiceMock(HttpEntity.class); + final Header header = EasyMock.createNiceMock(Header.class); + final ServletContext context = EasyMock.createNiceMock(ServletContext.class); + final GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class); + final ByteArrayInputStream backendResponse = new ByteArrayInputStream("knox-backend".getBytes( + StandardCharsets.UTF_8)); + + + EasyMock.expect(inboundResponse.getStatusLine()).andReturn(statusLine).anyTimes(); + EasyMock.expect(statusLine.getStatusCode()).andReturn(HttpStatus.SC_OK).anyTimes(); + EasyMock.expect(inboundResponse.getEntity()).andReturn(entity).anyTimes(); + EasyMock.expect(inboundResponse.getAllHeaders()).andReturn(new Header[0]).anyTimes(); + EasyMock.expect(inboundRequest.getServletContext()).andReturn(context).anyTimes(); + EasyMock.expect(entity.getContent()).andReturn(backendResponse).anyTimes(); + EasyMock.expect(entity.getContentType()).andReturn(header).anyTimes(); + EasyMock.expect(header.getElements()).andReturn(new HeaderElement[]{}).anyTimes(); + EasyMock.expect(entity.getContentLength()).andReturn(4L).anyTimes(); + EasyMock.expect(context.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE)).andReturn(config).anyTimes(); + + + 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 { + /* do nothing */ + } + }; + } + }).once(); + + CloseableHttpClient mockHttpClient = EasyMock.createNiceMock(CloseableHttpClient.class); + EasyMock.expect(mockHttpClient.execute(outboundRequest)).andReturn(inboundResponse).anyTimes(); + + EasyMock.replay(filterConfig, servletContext, outboundRequest, inboundRequest, + outboundResponse, mockHttpClient, inboundResponse, + statusLine, entity, header, context, config); + + + Assert.assertEquals(uri1.toString(), provider.getActiveURL(serviceName)); + ConfigurableHADispatch dispatch = new ConfigurableHADispatch(); + dispatch.setHttpClient(mockHttpClient); + dispatch.setHaProvider(provider); + dispatch.setServiceRole(serviceName); + dispatch.init(); + try { + dispatch.executeRequestWrapper(outboundRequest, inboundRequest, outboundResponse); + } catch (IOException e) { + //this is expected after the failover limit is reached + } + /* make sure the url is not ladbalanced since fallback did not happen */ + Assert.assertEquals(uri1.toString(), provider.getActiveURL(serviceName)); + } + + /** + * Test the case where loadbalancing is on + * Expected behavior: When loadbalncing is on then urls should loadbalance + * @throws Exception + */ + @Test + public void testLoadbalancingOn() throws Exception { + String serviceName = "OOZIE"; + HaDescriptor descriptor = HaDescriptorFactory.createDescriptor(); + descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig(serviceName, "true", "1", "1000", null, null, "true", null, null, null)); + HaProvider provider = new DefaultHaProvider(descriptor); + URI uri1 = new URI( "http://host1.valid" ); + URI uri2 = new URI( "http://host2.valid" ); + 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(); + + /* backend request */ + 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(); + + /* backend response */ + CloseableHttpResponse inboundResponse = EasyMock.createNiceMock(CloseableHttpResponse.class); + final StatusLine statusLine = EasyMock.createNiceMock(StatusLine.class); + final HttpEntity entity = EasyMock.createNiceMock(HttpEntity.class); + final Header header = EasyMock.createNiceMock(Header.class); + final ServletContext context = EasyMock.createNiceMock(ServletContext.class); + final GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class); + final ByteArrayInputStream backendResponse = new ByteArrayInputStream("knox-backend".getBytes( + StandardCharsets.UTF_8)); + + + EasyMock.expect(inboundResponse.getStatusLine()).andReturn(statusLine).anyTimes(); + EasyMock.expect(statusLine.getStatusCode()).andReturn(HttpStatus.SC_OK).anyTimes(); + EasyMock.expect(inboundResponse.getEntity()).andReturn(entity).anyTimes(); + EasyMock.expect(inboundResponse.getAllHeaders()).andReturn(new Header[0]).anyTimes(); + EasyMock.expect(inboundRequest.getServletContext()).andReturn(context).anyTimes(); + EasyMock.expect(entity.getContent()).andReturn(backendResponse).anyTimes(); + EasyMock.expect(entity.getContentType()).andReturn(header).anyTimes(); + EasyMock.expect(header.getElements()).andReturn(new HeaderElement[]{}).anyTimes(); + EasyMock.expect(entity.getContentLength()).andReturn(4L).anyTimes(); + EasyMock.expect(context.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE)).andReturn(config).anyTimes(); + + + 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 { + /* do nothing */ + } + }; + } + }).once(); + + CloseableHttpClient mockHttpClient = EasyMock.createNiceMock(CloseableHttpClient.class); + EasyMock.expect(mockHttpClient.execute(outboundRequest)).andReturn(inboundResponse).anyTimes(); + + EasyMock.replay(filterConfig, servletContext, outboundRequest, inboundRequest, + outboundResponse, mockHttpClient, inboundResponse, + statusLine, entity, header, context, config); + + + Assert.assertEquals(uri1.toString(), provider.getActiveURL(serviceName)); + ConfigurableHADispatch dispatch = new ConfigurableHADispatch(); + dispatch.setHttpClient(mockHttpClient); + dispatch.setHaProvider(provider); + dispatch.setServiceRole(serviceName); + dispatch.init(); + try { + dispatch.executeRequestWrapper(outboundRequest, inboundRequest, outboundResponse); + } catch (IOException e) { + //this is expected after the failover limit is reached + } + /* make sure the url is ladbalanced */ + Assert.assertEquals(uri2.toString(), provider.getActiveURL(serviceName)); + } + + /** + * Test the case where loadbalancing is on and sticky session is on + * Expected behavior: When loadbalncing is on and sticky session + * is on = urls should loadbalance with sticky session + * @throws Exception + */ + @Test + public void testLoadbalancingOnStickyOn() throws Exception { + String serviceName = "OOZIE"; + HaDescriptor descriptor = HaDescriptorFactory.createDescriptor(); + descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig(serviceName, "true", "1", "1000", null, null, "true", "true", null, null)); + HaProvider provider = new DefaultHaProvider(descriptor); + URI uri1 = new URI( "http://host1.valid" ); + URI uri2 = new URI( "http://host2.valid" ); + 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(); + + /* backend request */ + 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(); + + /* backend response */ + CloseableHttpResponse inboundResponse = EasyMock.createNiceMock(CloseableHttpResponse.class); + final StatusLine statusLine = EasyMock.createNiceMock(StatusLine.class); + final HttpEntity entity = EasyMock.createNiceMock(HttpEntity.class); + final Header header = EasyMock.createNiceMock(Header.class); + final ServletContext context = EasyMock.createNiceMock(ServletContext.class); + final GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class); + final ByteArrayInputStream backendResponse = new ByteArrayInputStream("knox-backend".getBytes( + StandardCharsets.UTF_8)); + + + EasyMock.expect(inboundResponse.getStatusLine()).andReturn(statusLine).anyTimes(); + EasyMock.expect(statusLine.getStatusCode()).andReturn(HttpStatus.SC_OK).anyTimes(); + EasyMock.expect(inboundResponse.getEntity()).andReturn(entity).anyTimes(); + EasyMock.expect(inboundResponse.getAllHeaders()).andReturn(new Header[0]).anyTimes(); + EasyMock.expect(inboundRequest.getServletContext()).andReturn(context).anyTimes(); + EasyMock.expect(entity.getContent()).andReturn(backendResponse).anyTimes(); + EasyMock.expect(entity.getContentType()).andReturn(header).anyTimes(); + EasyMock.expect(header.getElements()).andReturn(new HeaderElement[]{}).anyTimes(); + EasyMock.expect(entity.getContentLength()).andReturn(4L).anyTimes(); + EasyMock.expect(context.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE)).andReturn(config).anyTimes(); + + + 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 { + /* do nothing */ + } + }; + } + }).once(); + + CloseableHttpClient mockHttpClient = EasyMock.createNiceMock(CloseableHttpClient.class); + EasyMock.expect(mockHttpClient.execute(outboundRequest)).andReturn(inboundResponse).anyTimes(); + + EasyMock.replay(filterConfig, servletContext, outboundRequest, inboundRequest, + outboundResponse, mockHttpClient, inboundResponse, + statusLine, entity, header, context, config); + + + Assert.assertEquals(uri1.toString(), provider.getActiveURL(serviceName)); + ConfigurableHADispatch dispatch = new ConfigurableHADispatch(); + dispatch.setHttpClient(mockHttpClient); + dispatch.setHaProvider(provider); + dispatch.setServiceRole(serviceName); + dispatch.init(); + try { + dispatch.executeRequestWrapper(outboundRequest, inboundRequest, outboundResponse); + } catch (IOException e) { + //this is expected after the failover limit is reached + } + /* make sure the url is loadbalanced */ + Assert.assertEquals(uri2.toString(), provider.getActiveURL(serviceName)); + } + + /** + * Test the case where sticky session is on (and loadbalancing is on) + * Expected behavior: When + * @throws Exception + */ + @Test + public void testStickyOn() throws Exception { + String serviceName = "OOZIE"; + HaDescriptor descriptor = HaDescriptorFactory.createDescriptor(); + descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig(serviceName, "true", "1", "1000", null, null, "true", "true", null, null)); + HaProvider provider = new DefaultHaProvider(descriptor); + URI uri1 = new URI( "http://host1.valid" ); + URI uri2 = new URI( "http://host2.valid" ); + 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( uri2 ).anyTimes(); + EasyMock.expect(outboundRequest.getParams()).andReturn( params ).anyTimes(); + + /* backend request with cookie for url2 */ + //http://host2.valid = 59973e253ae20de796c6ef413608ec1c80fca24310a4cbdecc0ff97aeea55745 + Cookie[] cookie = new Cookie[] { new Cookie("KNOX_BACKEND-OOZIE","59973e253ae20de796c6ef413608ec1c80fca24310a4cbdecc0ff97aeea55745")}; + HttpServletRequest inboundRequest = EasyMock.createNiceMock(HttpServletRequest.class); + EasyMock.expect(inboundRequest.getRequestURL()).andReturn( new StringBuffer(uri2.toString()) ).once(); + EasyMock.expect(inboundRequest.getCookies()).andReturn( cookie ).anyTimes(); + 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(); + + /* backend response */ + CloseableHttpResponse inboundResponse = EasyMock.createNiceMock(CloseableHttpResponse.class); + final StatusLine statusLine = EasyMock.createNiceMock(StatusLine.class); + final HttpEntity entity = EasyMock.createNiceMock(HttpEntity.class); + final Header header = EasyMock.createNiceMock(Header.class); + final ServletContext context = EasyMock.createNiceMock(ServletContext.class); + final GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class); + final ByteArrayInputStream backendResponse = new ByteArrayInputStream("knox-backend".getBytes( + StandardCharsets.UTF_8)); + + + EasyMock.expect(inboundResponse.getStatusLine()).andReturn(statusLine).anyTimes(); + EasyMock.expect(statusLine.getStatusCode()).andReturn(HttpStatus.SC_OK).anyTimes(); + EasyMock.expect(inboundResponse.getEntity()).andReturn(entity).anyTimes(); + EasyMock.expect(inboundResponse.getAllHeaders()).andReturn(new Header[0]).anyTimes(); + EasyMock.expect(inboundRequest.getServletContext()).andReturn(context).anyTimes(); + EasyMock.expect(entity.getContent()).andReturn(backendResponse).anyTimes(); + EasyMock.expect(entity.getContentType()).andReturn(header).anyTimes(); + EasyMock.expect(header.getElements()).andReturn(new HeaderElement[]{}).anyTimes(); + EasyMock.expect(entity.getContentLength()).andReturn(4L).anyTimes(); + EasyMock.expect(context.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE)).andReturn(config).anyTimes(); + + + 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 { + /* do nothing */ + } + }; + } + }).once(); + + CloseableHttpClient mockHttpClient = EasyMock.createNiceMock(CloseableHttpClient.class); + EasyMock.expect(mockHttpClient.execute(outboundRequest)).andReturn(inboundResponse).anyTimes(); + + EasyMock.replay(filterConfig, servletContext, outboundRequest, inboundRequest, + outboundResponse, mockHttpClient, inboundResponse, + statusLine, entity, header, context, config); + + + Assert.assertEquals(uri1.toString(), provider.getActiveURL(serviceName)); + ConfigurableHADispatch dispatch = new ConfigurableHADispatch(); + dispatch.setHttpClient(mockHttpClient); + dispatch.setHaProvider(provider); + dispatch.setServiceRole(serviceName); + dispatch.init(); + try { + dispatch.executeRequestWrapper(outboundRequest, inboundRequest, outboundResponse); + } catch (IOException e) { + //this is expected after the failover limit is reached + } + /* sticky session is on do not loadbalance */ + Assert.assertEquals(uri1.toString(), provider.getActiveURL(serviceName)); + } + + @Test public void testConnectivityActive() throws Exception { String serviceName = "OOZIE";