You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by jo...@apache.org on 2016/04/27 20:15:17 UTC
ambari git commit: AMBARI-16131 - Prevent Views From Causing a Loss
of Service For Ambari (jonathanhurley)
Repository: ambari
Updated Branches:
refs/heads/trunk 4104f2f9d -> 85ac3c22e
AMBARI-16131 - Prevent Views From Causing a Loss of Service For Ambari (jonathanhurley)
Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/85ac3c22
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/85ac3c22
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/85ac3c22
Branch: refs/heads/trunk
Commit: 85ac3c22ea6aedd4a75e79ee25798677e07a7bdd
Parents: 4104f2f
Author: Jonathan Hurley <jh...@hortonworks.com>
Authored: Tue Apr 26 17:59:57 2016 -0400
Committer: Jonathan Hurley <jh...@hortonworks.com>
Committed: Wed Apr 27 14:07:17 2016 -0400
----------------------------------------------------------------------
.../server/configuration/Configuration.java | 81 ++++++---
.../ambari/server/controller/AmbariServer.java | 13 +-
.../ambari/server/utils/VersionUtils.java | 57 ++++--
.../apache/ambari/server/view/ViewRegistry.java | 8 +-
.../ambari/server/view/ViewThrottleFilter.java | 173 +++++++++++++++++++
.../server/view/ViewThrottleFilterTest.java | 150 ++++++++++++++++
6 files changed, 434 insertions(+), 48 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/ambari/blob/85ac3c22/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java b/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java
index 5ff6a74..0afae97 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java
@@ -17,11 +17,24 @@
*/
package org.apache.ambari.server.configuration;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.cert.CertificateException;
+import java.security.interfaces.RSAPublicKey;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.Set;
+
import org.apache.ambari.annotations.Experimental;
import org.apache.ambari.annotations.ExperimentalFeature;
import org.apache.ambari.server.AmbariException;
@@ -39,7 +52,6 @@ import org.apache.ambari.server.state.stack.OsFamily;
import org.apache.ambari.server.utils.AmbariPath;
import org.apache.ambari.server.utils.Parallel;
import org.apache.ambari.server.utils.ShellCommandUtil;
-import org.apache.commons.collections.ListUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.StringUtils;
@@ -47,23 +59,11 @@ import org.apache.commons.lang.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.security.cert.CertificateException;
-import java.security.interfaces.RSAPublicKey;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Properties;
-import java.util.Set;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
/**
@@ -537,6 +537,11 @@ public class Configuration {
private static final int VIEW_EXTRACTION_THREADPOOL_CORE_SIZE_DEFAULT = 10;
private static final String VIEW_EXTRACTION_THREADPOOL_TIMEOUT_KEY = "view.extraction.threadpool.timeout";
private static final long VIEW_EXTRACTION_THREADPOOL_TIMEOUT_DEFAULT = 100000L;
+ private static final String VIEW_REQUEST_THREADPOOL_MAX_SIZE_KEY = "view.request.threadpool.size.max";
+ private static final int VIEW_REQUEST_THREADPOOL_MAX_SIZE_DEFAULT = 0;
+ private static final String VIEW_REQUEST_THREADPOOL_TIMEOUT_KEY = "view.request.threadpool.timeout";
+ private static final int VIEW_REQUEST_THREADPOOL_TIMEOUT_DEFAULT = 2000;
+
public static final String PROPERTY_PROVIDER_THREADPOOL_MAX_SIZE_KEY = "server.property-provider.threadpool.size.max";
public static final int PROPERTY_PROVIDER_THREADPOOL_MAX_SIZE_DEFAULT = 4 * Runtime.getRuntime().availableProcessors();
@@ -1271,7 +1276,9 @@ public class Configuration {
* @param list
*/
private void listToLowerCase(List<String> list) {
- if (list == null) return;
+ if (list == null) {
+ return;
+ }
for (int i = 0; i < list.size(); i++) {
list.set(i, list.get(i).toLowerCase());
}
@@ -2324,6 +2331,32 @@ public class Configuration {
}
/**
+ * Get the maximum number of threads that will be allocated to fulfilling view
+ * requests.
+ *
+ * @return the maximum number of threads that will be allocated for requests
+ * to load views or {@value #VIEW_REQUEST_THREADPOOL_MAX_SIZE_DEFAULT}
+ * if not specified.
+ */
+ public int getViewRequestThreadPoolMaxSize() {
+ return Integer.parseInt(properties.getProperty(VIEW_REQUEST_THREADPOOL_MAX_SIZE_KEY,
+ String.valueOf(VIEW_REQUEST_THREADPOOL_MAX_SIZE_DEFAULT)));
+ }
+
+ /**
+ * Get the time, in ms, that a request to a view will wait for an available
+ * thread to handle the request before returning an error.
+ *
+ * @return the time that requests for a view should wait for an available
+ * thread or {@value #VIEW_REQUEST_THREADPOOL_TIMEOUT_DEFAULT} if not
+ * specified.
+ */
+ public int getViewRequestThreadPoolTimeout() {
+ return Integer.parseInt(properties.getProperty(VIEW_REQUEST_THREADPOOL_TIMEOUT_KEY,
+ String.valueOf(VIEW_REQUEST_THREADPOOL_TIMEOUT_DEFAULT)));
+ }
+
+ /**
* Get property-providers' thread pool core size.
*
* @return the property-providers' thread pool core size
http://git-wip-us.apache.org/repos/asf/ambari/blob/85ac3c22/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java
index dc53172..0527cbc 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java
@@ -19,9 +19,6 @@
package org.apache.ambari.server.controller;
-import javax.crypto.BadPaddingException;
-import javax.servlet.DispatcherType;
-
import java.io.File;
import java.io.IOException;
import java.net.Authenticator;
@@ -33,6 +30,9 @@ import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
+import javax.crypto.BadPaddingException;
+import javax.servlet.DispatcherType;
+
import org.apache.ambari.eventdb.webservice.WorkflowJsonService;
import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.StateRecoveryManager;
@@ -120,6 +120,7 @@ import org.apache.ambari.server.utils.StageUtils;
import org.apache.ambari.server.utils.VersionUtils;
import org.apache.ambari.server.view.ViewDirectoryWatcher;
import org.apache.ambari.server.view.ViewRegistry;
+import org.apache.ambari.server.view.ViewThrottleFilter;
import org.apache.velocity.app.Velocity;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.server.HttpConfiguration;
@@ -371,6 +372,12 @@ public class AmbariServer {
root.addFilter(new FilterHolder(injector.getInstance(AmbariViewsSecurityHeaderFilter.class)), "/api/v1/views/*",
DISPATCHER_TYPES);
+ // since views share the REST API threadpool, a misbehaving view could
+ // consume all of the available threads and effectively cause a loss of
+ // service for Ambari
+ root.addFilter(new FilterHolder(injector.getInstance(ViewThrottleFilter.class)),
+ "/api/v1/views/*", DISPATCHER_TYPES);
+
// session-per-request strategy for api
root.addFilter(new FilterHolder(injector.getInstance(AmbariPersistFilter.class)), "/api/*", DISPATCHER_TYPES);
root.addFilter(new FilterHolder(new MethodOverrideFilter()), "/api/*", DISPATCHER_TYPES);
http://git-wip-us.apache.org/repos/asf/ambari/blob/85ac3c22/ambari-server/src/main/java/org/apache/ambari/server/utils/VersionUtils.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/utils/VersionUtils.java b/ambari-server/src/main/java/org/apache/ambari/server/utils/VersionUtils.java
index b07f7da..d3d8592 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/utils/VersionUtils.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/utils/VersionUtils.java
@@ -17,12 +17,12 @@
*/
package org.apache.ambari.server.utils;
-import org.apache.ambari.server.bootstrap.BootStrapImpl;
-import org.apache.commons.lang.StringUtils;
-
import java.util.ArrayList;
import java.util.List;
+import org.apache.ambari.server.bootstrap.BootStrapImpl;
+import org.apache.commons.lang.StringUtils;
+
/**
* Provides various utility functions to be used for version handling.
* The compatibility matrix between server, agent, store can also be maintained
@@ -30,28 +30,35 @@ import java.util.List;
*/
public class VersionUtils {
/**
- * Compares two versions strings of the form N.N.N.N or even N.N.N.N-### (which should ignore everything after the dash).
- * If the user has a custom stack, e.g., 2.3.MYNAME or MYNAME.2.3, then any segment that contains letters should be ignored.
+ * Compares two versions strings of the form N.N.N.N or even N.N.N.N-###
+ * (which should ignore everything after the dash). If the user has a custom
+ * stack, e.g., 2.3.MYNAME or MYNAME.2.3, then any segment that contains
+ * letters should be ignored.
*
* @param version1
+ * the first operand. If set to {@value BootStrapImpl#DEV_VERSION}
+ * then this will always return {@code 0)}
* @param version2
- * @param maxLengthToCompare The maximum length to compare - 2 means only Major and Minor
- * 0 to compare the whole version strings
- * @return 0 if both are equal up to the length compared, -1 if first one is lower, +1 otherwise
+ * the second operand.
+ * @param maxLengthToCompare
+ * The maximum length to compare - 2 means only Major and Minor 0 to
+ * compare the whole version strings
+ * @return 0 if both are equal up to the length compared, -1 if first one is
+ * lower, +1 otherwise
*/
public static int compareVersions(String version1, String version2, int maxLengthToCompare)
throws IllegalArgumentException {
if (version1 == null){
throw new IllegalArgumentException("version1 cannot be null");
}
-
+
if (version2 == null){
throw new IllegalArgumentException("version2 cannot be null");
- }
-
+ }
+
version1 = StringUtils.trim(version1);
version2 = StringUtils.trim(version2);
-
+
if (version1.indexOf('-') >=0) {
version1 = version1.substring(0, version1.indexOf('-'));
}
@@ -69,7 +76,9 @@ public class VersionUtils {
throw new IllegalArgumentException("maxLengthToCompare cannot be less than 0");
}
- if(BootStrapImpl.DEV_VERSION.equals(version1.trim())) return 0;
+ if(BootStrapImpl.DEV_VERSION.equals(version1.trim())) {
+ return 0;
+ }
String[] version1Parts = version1.split("\\.");
String[] version2Parts = version2.split("\\.");
@@ -118,13 +127,20 @@ public class VersionUtils {
* Compares two versions strings of the form N.N.N.N
*
* @param version1
+ * the first operand. If set to {@value BootStrapImpl#DEV_VERSION}
+ * then this will always return {@code 0)}
* @param version2
- * @param allowEmptyVersions Allow one or both version values to be null or empty string
- * @return 0 if both are equal up to the length compared, -1 if first one is lower, +1 otherwise
+ * the second operand.
+ * @param allowEmptyVersions
+ * Allow one or both version values to be null or empty string
+ * @return 0 if both are equal up to the length compared, -1 if first one is
+ * lower, +1 otherwise
*/
public static int compareVersions(String version1, String version2, boolean allowEmptyVersions) {
if (allowEmptyVersions) {
- if (version1 != null && version1.equals(BootStrapImpl.DEV_VERSION)) return 0;
+ if (version1 != null && version1.equals(BootStrapImpl.DEV_VERSION)) {
+ return 0;
+ }
if (version1 == null && version2 == null) {
return 0;
} else {
@@ -155,7 +171,10 @@ public class VersionUtils {
* Compares two versions strings of the form N.N.N.N
*
* @param version1
+ * the first operand. If set to {@value BootStrapImpl#DEV_VERSION}
+ * then this will always return {@code 0)}
* @param version2
+ * the second operand.
* @return 0 if both are equal, -1 if first one is lower, +1 otherwise
*/
public static int compareVersions(String version1, String version2) {
@@ -166,8 +185,12 @@ public class VersionUtils {
* Compares two version for equality, allows empty versions
*
* @param version1
+ * the first operand. If set to {@value BootStrapImpl#DEV_VERSION}
+ * then this will always return {@code 0)}
* @param version2
- * @param allowEmptyVersions Allow one or both version values to be null or empty string
+ * the second operand.
+ * @param allowEmptyVersions
+ * Allow one or both version values to be null or empty string
* @return true if versions are equal; false otherwise
*/
public static boolean areVersionsEqual(String version1, String version2, boolean allowEmptyVersions) {
http://git-wip-us.apache.org/repos/asf/ambari/blob/85ac3c22/ambari-server/src/main/java/org/apache/ambari/server/view/ViewRegistry.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/view/ViewRegistry.java b/ambari-server/src/main/java/org/apache/ambari/server/view/ViewRegistry.java
index d23fcad..a312e6a 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/view/ViewRegistry.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/view/ViewRegistry.java
@@ -1677,8 +1677,8 @@ public class ViewRegistry {
protected boolean checkViewVersions(ViewEntity view, String serverVersion) {
ViewConfig config = view.getConfiguration();
- return checkViewVersion(view, config.getMinAmbariVersion(), serverVersion, "minimum", 1, "less than") &&
- checkViewVersion(view, config.getMaxAmbariVersion(), serverVersion, "maximum", -1, "greater than");
+ return checkViewVersion(view, config.getMinAmbariVersion(), serverVersion, "minimum", -1, "less than") &&
+ checkViewVersion(view, config.getMaxAmbariVersion(), serverVersion, "maximum", 1, "greater than");
}
@@ -1700,8 +1700,8 @@ public class ViewRegistry {
int index = version.indexOf('*');
- int compVal = index == -1 ? VersionUtils.compareVersions(version, serverVersion) :
- index > 0 ? VersionUtils.compareVersions(version.substring(0, index), serverVersion, index) : 0;
+ int compVal = index == -1 ? VersionUtils.compareVersions(serverVersion, version) :
+ index > 0 ? VersionUtils.compareVersions(serverVersion, version.substring(0, index), index) : 0;
if (compVal == errValue) {
String msg = "The Ambari server version " + serverVersion + " is " + errMsg + " the configured " + label +
http://git-wip-us.apache.org/repos/asf/ambari/blob/85ac3c22/ambari-server/src/main/java/org/apache/ambari/server/view/ViewThrottleFilter.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/view/ViewThrottleFilter.java b/ambari-server/src/main/java/org/apache/ambari/server/view/ViewThrottleFilter.java
new file mode 100644
index 0000000..b141fc9
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/view/ViewThrottleFilter.java
@@ -0,0 +1,173 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ambari.server.view;
+
+import java.io.IOException;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.ambari.server.configuration.Configuration;
+import org.eclipse.jetty.continuation.Continuation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/**
+ * The {@link ViewThrottleFilter} is used to ensure that views which misbehave
+ * do not cause a loss of service for Ambari. The underlying problem is that
+ * views are accessed off of the REST endpoint (/api/v1/views). This means that
+ * the Ambari REST API connector is going to handle the request from its own
+ * threadpool. There is no way to configure Jetty to use a different threadpool
+ * for the same connector. As a result, if a request to load a view holds the
+ * Jetty thread hostage, eventually we will see thread starvation and loss of
+ * service.
+ * <p/>
+ * An example of this situation is a view which makes an innocent request to a
+ * remote resource. If the view's request has a timeout of 60 seconds, then the
+ * Jetty thread is going to be held for that amount of time. With concurrent
+ * users and multiple instances of that view deployed, the Jetty threadpool can
+ * becomes exhausted quickly.
+ * <p/>
+ * Although there are more graceful ways of handling this situation, they mostly
+ * involve substantial re-architecture and design.
+ * <ul>
+ * <li>The use of a new connector and threadpool would require binding to
+ * another port for view requests. This will cause problems with "local" views
+ * and their assumption that if they run on the Ambari server they can share the
+ * same session.
+ * <li>The use of a {@link Continuation} in Jetty which can suspend the incoming
+ * request. We would need the ability for views to signal that they have
+ * completed their work in order to proceed with the suspended request.
+ * </ul>
+ */
+@Singleton
+public class ViewThrottleFilter implements Filter {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOG = LoggerFactory.getLogger(ViewThrottleFilter.class);
+
+ /**
+ * Used to determine the correct number of threads to allocate to view
+ * requests.
+ */
+ @Inject
+ private Configuration m_configuration;
+
+ /**
+ * Used to restrict how many REST API threads can be utilizied concurrently by
+ * view requests.
+ */
+ private Semaphore m_semaphore;
+
+ /**
+ * A timeout that a blocked view request should wait for an available thread
+ * before returning an error.
+ */
+ private int m_timeout;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ m_timeout = m_configuration.getViewRequestThreadPoolTimeout();
+
+ int clientThreadPoolSize = m_configuration.getClientThreadPoolSize();
+ int viewThreadPoolSize = m_configuration.getViewRequestThreadPoolMaxSize();
+
+ // start out using 1/2 of the available REST API request threads
+ int viewSemaphoreCount = clientThreadPoolSize / 2;
+
+ // if the size is specified, see if it's valid
+ if (viewThreadPoolSize > 0) {
+ viewSemaphoreCount = viewThreadPoolSize;
+
+ if (viewThreadPoolSize > clientThreadPoolSize) {
+ LOG.warn(
+ "The number of view processing threads ({}) cannot be greater than the REST API client threads {{})",
+ viewThreadPoolSize, clientThreadPoolSize);
+
+ viewSemaphoreCount = clientThreadPoolSize;
+ }
+ }
+
+ // log that we are restricting it
+ LOG.info("Ambari Views will be able to utilize {} concurrent REST API threads",
+ viewSemaphoreCount);
+
+ m_semaphore = new Semaphore(viewSemaphoreCount);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+
+ // do nothing if this is not an http request
+ if (!(request instanceof HttpServletRequest)) {
+ chain.doFilter(request, response);
+ return;
+ }
+
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+ boolean acquired = false;
+
+ try {
+ acquired = m_semaphore.tryAcquire(m_timeout, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException interruptedException) {
+ LOG.warn("While waiting for an available thread, the view request was interrupted");
+ }
+
+ if (!acquired) {
+ httpResponse.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
+ "There are no available threads to handle view requests");
+
+ // return to prevent the view's request from making it down any farther
+ return;
+ }
+
+ // let the request go through
+ try {
+ chain.doFilter(request, response);
+ } finally {
+ m_semaphore.release();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void destroy() {
+ }
+}
http://git-wip-us.apache.org/repos/asf/ambari/blob/85ac3c22/ambari-server/src/test/java/org/apache/ambari/server/view/ViewThrottleFilterTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/view/ViewThrottleFilterTest.java b/ambari-server/src/test/java/org/apache/ambari/server/view/ViewThrottleFilterTest.java
new file mode 100644
index 0000000..c1df50b
--- /dev/null
+++ b/ambari-server/src/test/java/org/apache/ambari/server/view/ViewThrottleFilterTest.java
@@ -0,0 +1,150 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ambari.server.view;
+
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.ambari.server.configuration.Configuration;
+import org.apache.ambari.server.state.stack.OsFamily;
+import org.easymock.EasyMock;
+import org.easymock.EasyMockSupport;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import com.google.inject.Binder;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+
+/**
+ * Tests the {@link ViewThrottleFilter}
+ */
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({ ViewThrottleFilter.class, Semaphore.class })
+public class ViewThrottleFilterTest extends EasyMockSupport {
+
+ private Injector m_injector;
+ private Semaphore m_mockSemaphore = createStrictMock(Semaphore.class);
+
+ /**
+ * @throws Exception
+ */
+ @Before
+ public void setup() throws Exception {
+ m_injector = Guice.createInjector(new MockModule());
+ PowerMockito.whenNew(Semaphore.class).withAnyArguments().thenReturn(m_mockSemaphore);
+ }
+
+ /**
+ * Tests that acquiring the {@link Semaphore} ensures that the
+ * {@link FilterChain} is invoked and that the {@link Semaphore} is eventually
+ * released.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testAcquireInvokesFilterChain() throws Exception {
+ Configuration configuration = m_injector.getInstance(Configuration.class);
+ EasyMock.expect(configuration.getViewRequestThreadPoolMaxSize()).andReturn(1).atLeastOnce();
+ EasyMock.expect(configuration.getViewRequestThreadPoolTimeout()).andReturn(2000).atLeastOnce();
+ EasyMock.expect(configuration.getClientThreadPoolSize()).andReturn(25).atLeastOnce();
+
+ // servlet mocks
+ HttpServletRequest request = createNiceMock(HttpServletRequest.class);
+ HttpServletResponse response = createStrictMock(HttpServletResponse.class);
+ FilterChain filterChain = createStrictMock(FilterChain.class);
+
+ // semaphore
+ EasyMock.expect(m_mockSemaphore.tryAcquire(2000, TimeUnit.MILLISECONDS)).andReturn(true);
+
+ // filter chain
+ filterChain.doFilter(request, response);
+ EasyMock.expectLastCall().once();
+
+ // semaphore release
+ m_mockSemaphore.release();
+ EasyMock.expectLastCall().once();
+
+ replayAll();
+
+ ViewThrottleFilter filter = new ViewThrottleFilter();
+ m_injector.injectMembers(filter);
+ filter.init(null);
+ filter.doFilter(request, response, filterChain);
+
+ verifyAll();
+ }
+
+ /**
+ * Tests that the failure to acquire the {@link Semaphore} stops the request
+ * and returns a {@link HttpServletResponse#SC_SERVICE_UNAVAILABLE}.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testSemaphorePreventsFilterChain() throws Exception {
+ Configuration configuration = m_injector.getInstance(Configuration.class);
+ EasyMock.expect(configuration.getViewRequestThreadPoolMaxSize()).andReturn(1).atLeastOnce();
+ EasyMock.expect(configuration.getViewRequestThreadPoolTimeout()).andReturn(2000).atLeastOnce();
+ EasyMock.expect(configuration.getClientThreadPoolSize()).andReturn(25).atLeastOnce();
+
+ // servlet mocks
+ HttpServletRequest request = createNiceMock(HttpServletRequest.class);
+ HttpServletResponse response = createStrictMock(HttpServletResponse.class);
+ FilterChain filterChain = createStrictMock(FilterChain.class);
+
+ // semaphore
+ EasyMock.expect(m_mockSemaphore.tryAcquire(2000, TimeUnit.MILLISECONDS)).andReturn(false);
+
+ // response
+ response.sendError(EasyMock.eq(HttpServletResponse.SC_SERVICE_UNAVAILABLE), EasyMock.anyString());
+ EasyMock.expectLastCall().once();
+
+ replayAll();
+
+ ViewThrottleFilter filter = new ViewThrottleFilter();
+ m_injector.injectMembers(filter);
+ filter.init(null);
+ filter.doFilter(request, response, filterChain);
+
+ verifyAll();
+ }
+
+ /**
+ *
+ */
+ private class MockModule implements Module {
+ /**
+ *
+ */
+ @Override
+ public void configure(Binder binder) {
+ binder.bind(Configuration.class).toInstance(createNiceMock(Configuration.class));
+ binder.bind(OsFamily.class).toInstance(createNiceMock(OsFamily.class));
+ }
+ }
+}