| /** |
| * 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() { |
| } |
| } |