/*
* Copyright (C) 2012 Facebook, 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 com.facebook.concurrency;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.joda.time.DateTimeUtils;
import java.util.List;
import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* This class puts its own Queue in front of another ExecutorService.
* It uses that service to submit tasks that drain its queue. It ensures
* a max # of drainer threads (default 1)
*
* Typical use case : back this by a SynchronousQueue based thread pool, ie
* Executors.newCachedThreadPool()
*
* This will allow more than one object to share the thread pool, but have
* a maximum # of threads. When there are no elements in the queue,
* no threads are used.
*
*/
public class ExecutorServiceFront extends AbstractExecutorService {
private final Lock lock = new ReentrantLock();
private final BlockingQueue<Runnable> workQueue;
private final ExecutorService executor;
private final String poolName;
private final long maxTimeSliceMillis;
private final BlockingQueue<Drainer> drainerList;
private static final Logger LOG = LoggerFactory.getLogger(ExecutorServiceFront.class);
/**
*
* @param workQueue
* @param executor
* @param maxDrainers
* @param maxTimeSlice - the maximum time slice of a drainer can run
* @param maxTimeSliceUnit - the unit of the maxTimeSlice argument
*/
public ExecutorServiceFront(
BlockingQueue<Runnable> workQueue,
ExecutorService executor,
String poolName,
int maxDrainers,
long maxTimeSlice,
TimeUnit maxTimeSliceUnit
) {
this.workQueue = workQueue;
this.executor = executor;
this.poolName = poolName;
this.maxTimeSliceMillis = maxTimeSliceUnit.toMillis(maxTimeSlice);
drainerList = new ArrayBlockingQueue<Drainer>(maxDrainers);
for (int i = 0; i < maxDrainers; i++) {
drainerList.add(new Drainer(String.format("%s-%03d", poolName, i)));
}
}
public ExecutorServiceFront(
BlockingQueue<Runnable> workQueue,
ExecutorService executor,
int maxDrainers,
long maxTimeSlice,
TimeUnit maxTimeSliceUnit
) {
this(workQueue, executor, "Drainer", maxDrainers, maxTimeSlice, maxTimeSliceUnit);
}
public ExecutorServiceFront(
BlockingQueue<Runnable> workQueue,
ExecutorService executor,
String poolName,
int maxDrainers
) {
this(workQueue, executor, poolName, maxDrainers, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
}
public ExecutorServiceFront(
BlockingQueue<Runnable> workQueue,
ExecutorService executor,
int maxDrainers
) {
this(workQueue, executor, "Drainer", maxDrainers, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
}
public ExecutorServiceFront(
ExecutorService executor,
long maxTimeSlice,
TimeUnit maxTimeSliceUnit
) {
this(
new LinkedBlockingQueue<Runnable>(),
executor,
"Drainer",
1,
maxTimeSlice,
maxTimeSliceUnit);
}
public ExecutorServiceFront(ExecutorService executor) {
this(new LinkedBlockingQueue<Runnable>(), executor, 1);
}
@Override
public void shutdown() {
throw new UnsupportedOperationException();
}
@Override
public synchronized List<Runnable> shutdownNow() {
throw new UnsupportedOperationException();
}
@Override
public boolean isShutdown() {
throw new UnsupportedOperationException();
}
@Override
public boolean isTerminated() {
throw new UnsupportedOperationException();
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public void execute(Runnable task) {
workQueue.offer(task);
lock.lock();
try {
if (!drainerList.isEmpty()) {
executor.execute(drainerList.poll());
}
} finally {
lock.unlock();
}
}
private class Drainer implements Runnable {
private final String threadName;
private Drainer(String threadName) {
this.threadName = threadName;
}
public void run() {
Thread t = Thread.currentThread();
String oldName = t.getName();
t.setName(threadName);
try {
internalRun();
} finally {
t.setName(oldName);
}
}
private void internalRun() {
long startTime = DateTimeUtils.currentTimeMillis();
while (DateTimeUtils.currentTimeMillis() - startTime < maxTimeSliceMillis) {
Runnable task = null;
lock.lock();
try {
task = workQueue.poll();
if (task == null) {
drainerList.add(this);
return;
}
} finally {
lock.unlock();
}
try {
task.run();
} catch (RuntimeException e) {
LOG.warn("Ignoring Task Failure", e);
}
}
lock.lock();
try {
// NOTE: if our queue is empty here, subsequent execute() will create new Drainers
// if need be. There is an edge case that executor is shutdown and we have no tasks,
// which is why we don't re-submit ourselves unless we have work to do (ie we haven't
// terminated
if (workQueue.isEmpty()) {
// if there's no work, this drainer expires
drainerList.add(this);
} else {
executor.execute(this);
}
} finally {
lock.unlock();
}
}
}
}