/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.flow;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geoserver.flow.config.DefaultControlFlowConfigurator;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.geoserver.filters.GeoServerFilter;
import org.geoserver.ows.AbstractDispatcherCallback;
import org.geoserver.ows.HttpErrorCodeException;
import org.geoserver.ows.Request;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.Operation;
import org.geotools.util.logging.Logging;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
/**
* Callback that controls the flow of OWS requests based on user specified rules and makes sure
* GeoServer does not get overwhelmed by too many concurrent ones. Can also be used to provide
* different quality of service on different users.
*
* @author Andrea Aime - OpenGeo
*/
public class ControlFlowCallback extends AbstractDispatcherCallback implements ApplicationContextAware, GeoServerFilter {
/**
* Header added to all responses to make it visible how much deplay was applied going thorough
* the flow controllers
*/
static final String X_RATELIMIT_DELAY = "X-Control-flow-delay-ms";
static final Logger LOGGER = Logging.getLogger(ControlFlowCallback.class);
/**
* Container for the original Request object, the controllers and the timeout (to make
* sure we are playing with the same objects, regardless of what other machinery
* might do to mock up with the Dispatcher.REQUEST thread local).
*/
static final class CallbackContext {
List<FlowController> controllers;
long timeout;
Request request;
int nestingLevel = 1;
public CallbackContext(Request request, List<FlowController> controllers, long timeout) {
this.controllers = controllers;
this.timeout = timeout;
this.request = request;
}
}
static ThreadLocal<CallbackContext> REQUEST_CONTROLLERS = new ThreadLocal<CallbackContext>();
FlowControllerProvider provider;
AtomicLong blockedRequests = new AtomicLong();
AtomicLong runningRequests = new AtomicLong();
public ControlFlowCallback() {
// this is just to isolate tests from shared state, at runtime there is only one callback.
REQUEST_CONTROLLERS.remove();
}
private ApplicationContext applicationContext;
/**
* Returns the current number of blocked/queued requests.
*/
public long getBlockedRequests() {
return blockedRequests.get();
}
/**
* Returns the current number of running requests.
*/
public long getRunningRequests() {
return runningRequests.get();
}
public Operation operationDispatched(Request request, Operation operation) {
// if this request is nested, release the previous controllers and grab new ones
// Nesting happens only with integrated GWC, sometimes the nested request is similar to the
// outside one, e.g., with transparent integration, other times it's completely different, e.g. native
// tile services). We cannot afford to have the same controller lock twice, that would cause deadlocks
if (REQUEST_CONTROLLERS.get() != null) {
if(LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Nested request found, not locking on it");
}
REQUEST_CONTROLLERS.get().nestingLevel++;
return operation;
}
blockedRequests.incrementAndGet();
long start = System.currentTimeMillis();
try {
// the operation has not been set in the Request yet by the dispatcher, do so now in
// a clone of the Request
Request requestWithOperation = null;
if(request != null) {
requestWithOperation = new Request(request);
requestWithOperation.setOperation(operation);
}
// grab the controllers for this request
List<FlowController> controllers = null;
try {
controllers = provider.getFlowControllers(requestWithOperation);
} catch (Exception e) {
LOGGER.log(Level.SEVERE,
"An error occurred setting up the flow controllers to this request", e);
return operation;
}
if (controllers.size() == 0) {
LOGGER.info("Control-flow inactive, there are no configured rules");
} else {
if(LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Request [" + requestWithOperation + "] starting, processing through flow controllers");
}
long timeout = provider.getTimeout(requestWithOperation);
CallbackContext context = new CallbackContext(requestWithOperation, controllers, timeout);
REQUEST_CONTROLLERS.set(context);
long maxTime = timeout > 0 ? System.currentTimeMillis() + timeout : -1;
for (FlowController flowController : controllers) {
if (timeout > 0) {
long maxWait = maxTime - System.currentTimeMillis();
if(LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Request [" + requestWithOperation + "] checking flow controller " + flowController);
}
if (!flowController.requestIncoming(requestWithOperation, maxWait)) {
throw new HttpErrorCodeException(503,
"Requested timeout out while waiting to be executed, please lower your request rate");
}
if(LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Request [" + requestWithOperation + "] passed flow controller " + flowController);
}
} else {
flowController.requestIncoming(requestWithOperation, -1);
}
}
}
} finally {
blockedRequests.decrementAndGet();
runningRequests.incrementAndGet();
if(REQUEST_CONTROLLERS.get() != null) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Request started, running requests: " + getRunningRequests() + ", blocked requests: "
+ getBlockedRequests());
}
}
if (request != null && request.getHttpResponse() != null) {
// report how much time was spent going though the flow controllers
long end = System.currentTimeMillis();
request.getHttpResponse().addHeader(X_RATELIMIT_DELAY,
String.valueOf(end - start));
}
}
return operation;
}
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
if (applicationContext instanceof ConfigurableApplicationContext) {
// register default beans if needed
registDefaultBeansIfNeeded((ConfigurableApplicationContext) applicationContext);
} else {
// we cannot regist default beans, there is nothing else we can do about this
LOGGER.warning("Application context not configurable, control-flow default beans will not be registered.");
}
provider = GeoServerExtensions.bean(FlowControllerProvider.class, applicationContext);
// default beans may have not been registered
if (provider == null) {
provider = new DefaultFlowControllerProvider(applicationContext);
}
}
/**
* Register default beans for control flow configurator and flow controller.
*/
private void registDefaultBeansIfNeeded(ConfigurableApplicationContext applicationContext) {
ConfigurableListableBeanFactory factory = applicationContext.getBeanFactory();
// make sure defautl beans are only registered once
synchronized (ControlFlowCallback.class) {
// first handle the configurator bean
try {
applicationContext.getBean(ControlFlowConfigurator.class, applicationContext);
} catch (NoSuchBeanDefinitionException exception) {
// we need to use the default configurator
factory.registerSingleton("defaultControlFlowConfigurator", new DefaultControlFlowConfigurator());
LOGGER.fine("Defautl flow configurator bean dynamically registered.");
}
// handle the flow controller provider bean
try {
applicationContext.getBean(FlowControllerProvider.class, applicationContext);
} catch (NoSuchBeanDefinitionException exception) {
// we need to use the default flow controller provider
factory.registerSingleton("defaultFlowControllerProvider", new DefaultFlowControllerProvider(applicationContext));
LOGGER.fine("Defautl flow controller provider bean dynamically registered.");
}
}
}
public void init(FilterConfig filterConfig) throws ServletException {
// nothing to do
}
public void finished(Request request) {
releaseControllers(false);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
// execute normally
chain.doFilter(request, response);
} finally {
// this is a precaution in case finished() is not called by any reason
releaseControllers(true);
}
}
private void releaseControllers(boolean forceRelease) {
CallbackContext context = REQUEST_CONTROLLERS.get();
try {
// will be called twice in normal requests, make sure we check if there
// are actually controllers around
if (context != null) {
context.nestingLevel--;
if(context.nestingLevel <= 0 || forceRelease) {
runningRequests.decrementAndGet();
// call back the same controllers we used when the operation started, releasing
// them in inverse order
LOGGER.info("releasing flow controllers for [" + context.request + "]");
final List<FlowController> controllers = context.controllers;
for (int i = controllers.size() - 1; i >= 0; i--) {
FlowController flowController = controllers.get(i);
try {
flowController.requestComplete(context.request);
} catch (Throwable t) {
// catching throwable here is intended, we cannot afford not to
// release controllers, it would eventually lead to deadlock
LOGGER.log(Level.SEVERE, "Flow controller " + flowController
+ " failed to mark the request as complete", t);
}
}
// provide some visibility that control flow is running
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Request completed, running requests: " + getRunningRequests()
+ ", blocked requests: " + getBlockedRequests());
}
}
}
} finally {
// clean up the thread local, all controllers have been released
if(context != null && (context.nestingLevel <= 0 || forceRelease)) {
REQUEST_CONTROLLERS.remove();
}
}
}
@Override
public void destroy() {
// nothing to do
}
}