/*
* Copyright 2014 Google Inc. All rights reserved.
*
*
* 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 com.google.maps.internal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Rate Limit Policy for Google Maps Web Services APIs.
*/
public class RateLimitExecutorService implements ExecutorService, Runnable {
private static final Logger LOG = LoggerFactory.getLogger(RateLimitExecutorService.class.getName());
private static final int DEFAULT_QUERIES_PER_SECOND = 10;
private static final int SECOND = 1000;
private static final int HALF_SECOND = SECOND / 2;
// It's important we set Ok's second arg to threadFactory(.., true) to ensure the threads are
// killed when the app exits. For synchronous requests this is ideal but it means any async
// requests still pending after termination will be killed.
private final ExecutorService delegate = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), Integer.MAX_VALUE, 60,
TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
threadFactory("Rate Limited Dispatcher", true));
private final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>();
private volatile int queriesPerSecond;
private volatile int minimumDelay;
private LinkedList<Long> sentTimes = new LinkedList<Long>();
private long lastSentTime = 0;
public RateLimitExecutorService() {
setQueriesPerSecond(DEFAULT_QUERIES_PER_SECOND);
Thread delayThread = new Thread(this);
delayThread.setDaemon(true);
delayThread.setName("RateLimitExecutorDelayThread");
delayThread.start();
}
public void setQueriesPerSecond(int maxQps) {
this.queriesPerSecond = maxQps;
this.minimumDelay = HALF_SECOND / queriesPerSecond;
}
public void setQueriesPerSecond(int maxQps, int minimumInterval) {
this.queriesPerSecond = maxQps;
this.minimumDelay = minimumInterval;
LOG.info("Configuring rate limit at QPS: {} , minimum delay {} ms between requests",maxQps,minimumInterval);
}
/**
* Main loop.
*/
@Override
public void run() {
try {
while (!delegate.isShutdown()) {
long now = System.currentTimeMillis();
long oneSecondAgo = now - SECOND;
Runnable r = queue.take();
long requiredSeparationDelay = lastSentTime + minimumDelay - now;
if (requiredSeparationDelay > 0) {
Thread.sleep(requiredSeparationDelay);
}
// Purge any sent times older than a second
while (sentTimes.size() > 0 && sentTimes.peekFirst() < oneSecondAgo) {
sentTimes.pop();
}
long delay = 0;
if (sentTimes.size() > 0) {
delay = sentTimes.peekFirst() + SECOND - System.currentTimeMillis();
}
if (sentTimes.size() < queriesPerSecond || delay <= 0) {
delegate.execute(r);
lastSentTime = now;
sentTimes.add(lastSentTime);
} else {
queue.add(r);
Thread.sleep(delay);
}
}
} catch (InterruptedException ie) {
LOG.info("Interrupted", ie);
}
}
private static ThreadFactory threadFactory(final String name, final boolean daemon) {
return new ThreadFactory() {
@Override
public Thread newThread(Runnable runnable) {
Thread result = new Thread(runnable, name);
result.setDaemon(daemon);
return result;
}
};
}
@Override
public void execute(Runnable runnable) {
queue.add(runnable);
}
// Everything below here is straight delegation.
@Override
public void shutdown() {
delegate.shutdown();
}
@Override
public List<Runnable> shutdownNow() {
return delegate.shutdownNow();
}
@Override
public boolean isShutdown() {
return delegate.isShutdown();
}
@Override
public boolean isTerminated() {
return delegate.isTerminated();
}
@Override
public boolean awaitTermination(long l, TimeUnit timeUnit) throws InterruptedException {
return delegate.awaitTermination(l, timeUnit);
}
@Override
public <T> Future<T> submit(Callable<T> tCallable) {
return delegate.submit(tCallable);
}
@Override
public <T> Future<T> submit(Runnable runnable, T t) {
return delegate.submit(runnable, t);
}
@Override
public Future<?> submit(Runnable runnable) {
return delegate.submit(runnable);
}
@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> callables)
throws InterruptedException {
return delegate.invokeAll(callables);
}
@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> callables, long l,
TimeUnit timeUnit) throws InterruptedException {
return delegate.invokeAll(callables, l, timeUnit);
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> callables) throws InterruptedException,
ExecutionException {
return delegate.invokeAny(callables);
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> callables, long l, TimeUnit timeUnit)
throws InterruptedException, ExecutionException, TimeoutException {
return delegate.invokeAny(callables, l, timeUnit);
}
}