/** * Copyright 2008 Google Inc. * * Licensed 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.waveprotocol.wave.client.scheduler; import com.google.gwt.core.client.GWT; import org.waveprotocol.wave.client.common.util.FastQueue; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Queue; /** * An implementation of the standard token bucket rate limiter. * See http://en.wikipedia.org/wiki/Token_bucket for more detail. * */ public class BucketRateLimiter { /** * The maximum number of tokens stored in the bucket. */ private final int maxStoredTokens; /** * The maximum number of outstanding requests allowed. If more than this is * added to the queue, the oldest one will get cancelled. */ private final int maxOutstandingRequests; /** * The period at which tokens are added to the bucket. */ private final int tokenPeriodMs; /** * The scheduling interval between the timer delay. */ private final int schedulingInterval; /** * The timer service used to execute the command when a new token is * available. */ private final TimerService service; // This implementation must be suitable for FIFO behaviour private final Queue<CancellableCommand> outstandingRequests = GWT.isClient() ? new FastQueue<CancellableCommand>() : new LinkedList<CancellableCommand>(); /** * The time that last token was given out. */ private int lastTokenTime; /** * Number of available tokens */ private int numTokens; /** * true if task is currently being run by timer. */ private boolean isScheduled; private final Scheduler.Task task = new Scheduler.Task() { /** Used for toString() so we can report was was executed and what took too long */ private final List<CancellableCommand> lastTasks = new ArrayList<CancellableCommand>(); public void execute() { lastTasks.clear(); updateTokenCount(); while (numTokens > 0 && outstandingRequests.size() > 0) { numTokens--; CancellableCommand command = outstandingRequests.poll(); lastTasks.add(command); command.execute(); } // reschedule the timer. if (outstandingRequests.size() > 0) { service.scheduleDelayed(this, schedulingInterval); } else { // limit the number of stored token numTokens = Math.min(numTokens, maxStoredTokens); isScheduled = false; } } @Override public String toString() { return "BucketRateLimiter. [Last tasks: " + lastTasks + "]"; } }; /** * Create a new BucketRateLimiter, with numTokens = 0. * * @param service TimerService to use for scheduling. * @param maxOutstandingRequests The maximum number of command that can exist * in the limiter. if more than this number of outstanding requests has * been added, the oldest ones will get cancelled until the number of * scheduled command is belong this limit. * @param maxStoredTokens The maximum number of token that can be saved up. * @param tokenPeriodMs */ public BucketRateLimiter(TimerService service, int maxOutstandingRequests, int maxStoredTokens, int tokenPeriodMs) { this(service, maxOutstandingRequests, maxStoredTokens, 0, tokenPeriodMs); } /** * Create a new BucketRateLimiter. * * @param service TimerService to use for scheduling. * @param maxOutstandingRequests The maximum number of command that can exist * in the limiter. if more than this number of outstanding requests has * been added, the oldest ones will get cancelled until the number of * scheduled command is belong this limit. * @param maxStoredTokens The maximum number of token that can be saved up. * @param numTokens The number of tokens that the BucketRateLimiter starts with. * @param tokenPeriodMs */ public BucketRateLimiter(TimerService service, int maxOutstandingRequests, int maxStoredTokens, int numTokens, int tokenPeriodMs) { this.maxStoredTokens = maxStoredTokens; this.tokenPeriodMs = tokenPeriodMs; // For simplicity, we assume that the schedulingInterval is the same as tokenPeriodMs. this.schedulingInterval = tokenPeriodMs; this.service = service; this.maxOutstandingRequests = maxOutstandingRequests; this.lastTokenTime = service.elapsedMillis(); this.numTokens = numTokens; } /** * Schedule a rate limited command. */ public void schedule(CancellableCommand command) { if (isScheduled) { // the bucket is still waiting for more tokens (hence scheduled) addCommandToQueue(command); } else { // work out the number of tokens should be available updateTokenCount(); // clip it at the maximum stored amount. numTokens = Math.min(numTokens, maxStoredTokens); // if there is a token available, fire event immediately with the token. if (numTokens > 0) { numTokens--; // Queue must be empty (because non-empty queue implies isScheduled), so we can // run the command directly, rather than task.execute(), because there is no need // to reschedule afterwards. command.execute(); } else { // Not enough tokens - schedule to run later. addCommandToQueue(command); schedule(); } } } /** * Schedules this task against the timer service. */ private void schedule() { service.scheduleDelayed(task, schedulingInterval); isScheduled = true; } /** * Add the cancelable command to the queue of commands to execute. * @param command */ private void addCommandToQueue(CancellableCommand command) { outstandingRequests.add(command); while (outstandingRequests.size() > maxOutstandingRequests) { outstandingRequests.poll().onCancelled(); } } /** * Update the number of tokens. */ private void updateTokenCount() { // add new tokens based on elapsed time int now = service.elapsedMillis(); int timeElapsed = now - lastTokenTime; if (timeElapsed >= tokenPeriodMs) { numTokens += timeElapsed / tokenPeriodMs; lastTokenTime = now - timeElapsed % tokenPeriodMs; } } /** * Clear the available tokens and the token timer. */ public void clearTokens() { numTokens = 0; lastTokenTime = service.elapsedMillis(); // TODO(oshlack/reuben): Change this to use scheduler.isScheduled(task) once it exists. if (isScheduled) { service.scheduleDelayed(task, schedulingInterval); } } /** * Cancel all scheduled commands. */ public void cancelAll() { terminateScheduleTask(); for (CancellableCommand request : outstandingRequests) { request.onCancelled(); } outstandingRequests.clear(); } /** * Cancel a scheduled command. * * @return true if the command is a part of the queue and it is cancelled. */ public boolean cancel(CancellableCommand command) { if (outstandingRequests.remove(command)) { command.onCancelled(); return true; } return false; } private void terminateScheduleTask() { if (isScheduled) { service.cancel(task); isScheduled = false; } } public void executeAll() { terminateScheduleTask(); for (CancellableCommand request : outstandingRequests) { request.execute(); } outstandingRequests.clear(); } }