/* (c) 2014 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.controller; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import org.geoserver.flow.ControlFlowCallback; import org.geoserver.ows.Request; import org.geotools.util.logging.Logging; /** * A flow controller setting a cookie on HTTP request and making sure the same user cannot do more * than X requests in parallel. Warning: if a client does not support cookies this class cannot work * properly and will start accumulating queues with just one item inside. As a workaround when too * many queues are accumulated a scan starts that purges all queues that are empty and have not been * touched within a given amount of time: the idea is that a past that time we're assuming the * client is no more working actively against the server and the queue can thus be removed. * * @author Andrea Aime - OpenGeo * @author Juan Marin, OpenGeo */ public class UserConcurrentFlowController extends QueueController { static final Logger LOGGER = Logging.getLogger(ControlFlowCallback.class); /** * Thread local holding the current request queue id TODO: consider having a user map in * {@link Request} instead */ static ThreadLocal<String> QUEUE_ID = new ThreadLocal<String>(); CookieKeyGenerator keyGenerator = new CookieKeyGenerator(); /** * Last time we've performed a queue cleanup */ long lastCleanup = System.currentTimeMillis(); /** * Number of queues at which we start looking for purging stale ones */ int maxQueues = 100; /** * Time it takes for an inactive queue to be considered stale */ int maxAge = 10000; /** * Builds a UserFlowController that will trigger stale queue expiration once 100 queues have * been accumulated and * * @param queueSize the maximum amount of per user concurrent requests */ public UserConcurrentFlowController(int queueSize) { this(queueSize, 100, 10000); } /** * Builds a new {@link UserConcurrentFlowController} * * @param queueSize the maximum amount of per user concurrent requests * @param maxQueues the number of accumulated user queues that will trigger a queue cleanup * @param maxAge the max quiet time for an empty queue to be considered stale and removed */ public UserConcurrentFlowController(int queueSize, int maxQueues, int maxAge) { this.queueSize = queueSize; this.maxQueues = maxQueues; this.maxAge = maxAge; } @Override public void requestComplete(Request request) { String queueId = QUEUE_ID.get(); QUEUE_ID.remove(); if (queueId != null) { BlockingQueue<Request> queue = queues.get(queueId); if (queue != null) queue.remove(request); } } public boolean requestIncoming(Request request, long timeout) { boolean retval = true; long now = System.currentTimeMillis(); String queueId = keyGenerator.getUserKey(request); QUEUE_ID.set(queueId); // see if we have that queue already, otherwise generate it TimedBlockingQueue queue = null; queue = queues.get(queueId); if (queue == null) { queue = new TimedBlockingQueue(queueSize, true); queues.put(queueId, queue); } // queue token handling try { if (timeout > 0) { retval = queue.offer(request, timeout, TimeUnit.MILLISECONDS); } else { queue.put(request); } } catch (InterruptedException e) { LOGGER.log(Level.WARNING, "Unexpected interruption while " + "blocking on the request queue"); } if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("UserFlowController(" + queueSize + "," + queueId + ") queue size " + queue.size()); LOGGER.fine("UserFlowController(" + queueSize + "," + queueId + ") total queues " + queues.size()); } // cleanup stale queues if necessary if ((queues.size() > maxQueues && (now - lastCleanup) > (maxAge / 10)) || (now - lastCleanup) > maxAge) { int cleanupCount = 0; synchronized (queues) { for (String key : queues.keySet()) { TimedBlockingQueue tbq = queues.get(key); if (now - tbq.lastModified > maxAge && tbq.size() == 0) { queues.remove(key); cleanupCount++; } } lastCleanup = now; if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("UserFlowController(" + queueSize + ") purged " + cleanupCount + " stale queues"); } } } return retval; } }