/*
* Copyright 2009-2010 Brian S O'Neill
*
* 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.cojen.dirmi.util;
import java.util.LinkedList;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import org.cojen.util.ThrowUnchecked;
/**
* Utility class which can throttle the activity of {@link
* org.cojen.dirmi.Asynchronous Asynchronous} methods. Without a throttling
* mechanism, {@code Asynchronous} methods can cause too many threads to
* execute or starve other activities.
*
* @author Brian S O'Neill
*/
public class ThrottledExecutor implements Executor {
private final int mPermits;
private final int mMaxQueued;
private final LinkedList<Runnable> mQueue;
private int mActive;
/**
* @param permits maximum number of threads allowed to execute commands concurrently
* @param maxQueued maximum number of queued commands allowed before rejecting new ones
*/
public ThrottledExecutor(int permits, int maxQueued) {
if (permits < 1) {
throw new IllegalArgumentException("Permits must be at least one: " + permits);
}
if (maxQueued < 0) {
throw new IllegalArgumentException("Maximum queued cannot be negative: " + maxQueued);
}
mPermits = permits;
mMaxQueued = maxQueued;
mQueue = new LinkedList<Runnable>();
}
/**
* Queues the command for later execution, or executes it immediately. If
* the queue contains more commands, this method will attempt to drain the
* queue before returning. If new commands are constantly being enqueued,
* this method might never return.
*
* @throws RejectedExecutionException if too many commands are queued
*/
public void execute(Runnable command) throws RejectedExecutionException {
if (command == null) {
throw new NullPointerException("Command is null");
}
LinkedList<Runnable> queue = mQueue;
synchronized (this) {
int active = mActive;
if (active >= mPermits) {
if (queue.size() >= mMaxQueued) {
throw new RejectedExecutionException("Too many queued commands");
}
queue.add(command);
return;
}
if (!queue.isEmpty()) {
queue.add(command);
command = null;
}
mActive = ++active;
}
while (true) {
if (command != null) {
try {
command.run();
} catch (Throwable e) {
try {
Thread t = Thread.currentThread();
t.getUncaughtExceptionHandler().uncaughtException(t, e);
} catch (Throwable e2) {
// Good job, uncaught exception handler.
synchronized (this) {
if (queue.isEmpty() || mActive > 1) {
// Safe to exit.
mActive--;
ThrowUnchecked.fire(e2);
return;
}
// Cannot exit because commands need to drain, so
// ignore the second exception.
}
}
}
}
synchronized (this) {
if ((command = queue.poll()) == null) {
mActive--;
return;
}
}
}
}
}