blob: b141fc9698537406da2b4ff296d5213a7a65c4fb [file] [log] [blame]
/**
* 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() {
}
}