com.tomtom.speedtools.thread.WorkQueue Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of core Show documentation
Show all versions of core Show documentation
Consists of a lot of handy classes and utilities for your main Java application, like
buffers, checksum calculations, locale handling, time conversion and more.
/*
* Copyright (C) 2012-2019, TomTom (http://tomtom.com).
*
* 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.tomtom.speedtools.thread;
import com.tomtom.speedtools.time.UTCTime;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.*;
/**
* This class creates a pool of worker threads that will execute workload tasks. The amount of actual threads is
* limited, as well as the workload queue.
*
* To use this class, you should create a WorkQueue with a maximum number of work packages for the work queue. Some of
* the worker threads are started (and stand-by) immediately after creating the WorkQueue.
*
* After that, you can add workload (which is really an instance of a Runnable) to the work queue by using
* startOrWait(workLoad). This call will add the workload if the queue is not filled up yet, or wait until there is room
* to add the workload.
*
* You can use waitUntilFinished() to wait until the entire workload queue is processed (and all worker threads are done
* processing). Or you can check whether processing is done with isEmptyAndFinished().
*
* If worker threads throw exceptions, these are caught and stored in a list which can be retrieved by
* getRuntimeExceptions(). The worker thread that processed workload throwing such an exception is simply returned to
* the thread pool ready to process the next piece of workload.
*
* To shutdown all the worker threads, use scheduleShutdown(). This will schedule a shutdown of all worker threads after
* they have finished processing their workload. After this call, the only valid calls left is getRuntimeExceptions().
*
* Important: This class itself is NOT thread safe: only 1 thread should feed one particular instance of WorkQueue at a
* time.
*/
public class WorkQueue {
private static final Logger LOG = LoggerFactory.getLogger(WorkQueue.class);
private static final int ISSUE_WAITING_LOG_LINE_AFTER_SECS = 10; // Issue 'debug' log every now and then.
private static final int MAX_THREADS_FOR_FULL_QUEUE = 32;
private static final int BUSY_WAIT_MSECS_MIN = 5;
private static final int BUSY_WAIT_MSECS_MAX = 250;
@Nonnull
private ThreadPoolExecutor executor;
@Nonnull
private final List exceptions;
private final int maxQueueSize;
private final long feederThread;
/**
* Create a work queue with a maximum number of worker threads and a maximum workload queue size. Adding workload
* past the workload queue size will block until the queue is small enough to add more workload.
*
* The caller should call shutdown() to shut down the threads after they have carried out their workloads.
*
* @param maxQueueSize Maximum work load queue size.
*/
public WorkQueue(
final int maxQueueSize) {
this.exceptions = Collections.synchronizedList(new ArrayList<>());
this.feederThread = Thread.currentThread().getId();
this.maxQueueSize = maxQueueSize;
this.executor = createNewExecutor();
}
/**
* Schedule shutdown for all threads after they finished their work. After this call, no other calls to this class
* should be made!
*/
public void scheduleShutdown() {
assert Thread.currentThread().getId() == feederThread;
executor.shutdown();
}
/**
* Start workload, or wait if there is too much workload in the queue.
*
* @param workLoad Workload to be started.
* @param timeout Timeout in millis. If there is no room left in the queue before this timeout expires, the
* workload is discarded and not scheduled. Use 0 for wait 'forever'.
*/
@SuppressWarnings("CallToNotifyInsteadOfNotifyAll")
public void startOrWait(@Nonnull final Runnable workLoad, final long timeout) {
assert timeout >= 0;
assert !executor.isShutdown();
assert Thread.currentThread().getId() == feederThread;
final DateTime startTime = UTCTime.now();
final long start = startTime.getMillis();
DateTime nextDebugTime = startTime.plusSeconds(ISSUE_WAITING_LOG_LINE_AFTER_SECS);
boolean scheduled = false;
boolean again = false;
int busyWait = BUSY_WAIT_MSECS_MIN;
do {
try {
executor.execute(new RuntimeExceptionCatcher(workLoad));
scheduled = true;
} catch (final RejectedExecutionException ignored1) {
assert !scheduled;
try {
//noinspection BusyWait
Thread.sleep(busyWait);
if (busyWait < BUSY_WAIT_MSECS_MAX) {
++busyWait;
}
final DateTime now = UTCTime.now();
final long timeWaiting = now.getMillis() - start;
again = ((timeout == 0) || (timeWaiting < timeout));
// Issue a log message only if timeout == 0 and task is rescheduled.
if (again && (timeout == 0) && now.isAfter(nextDebugTime)) {
LOG.debug("startOrWait: workLoad not executed yet, already waiting {} secs...",
timeWaiting / 1000);
nextDebugTime = now.plusSeconds(ISSUE_WAITING_LOG_LINE_AFTER_SECS);
}
} catch (final InterruptedException ignored2) {
assert !again;
}
}
}
while (!scheduled && again);
if (!scheduled) {
LOG.debug("startOrWait: workLoad was not scheduled, aborted after timeout={} msecs", timeout);
}
}
/**
* Start workload, or wait if there is too much workload in the queue.
*
* @param workLoad Workload to be started.
*/
public void startOrWait(@Nonnull final Runnable workLoad) {
startOrWait(workLoad, 0);
}
/**
* Wait until the work pool finished executing all work load.
*
* @param timeout Max. wait time in msecs. Use 0 for 'forever'.
* @return False if exceptions were caught during executing workload packages.
*/
public boolean waitUntilFinished(final long timeout) {
assert timeout >= 0;
assert !executor.isShutdown();
assert Thread.currentThread().getId() == feederThread;
LOG.debug("waitUntilFinished: shut down executor");
executor.shutdown();
final DateTime startTime = UTCTime.now();
DateTime nextDebugTime = startTime.plusSeconds(ISSUE_WAITING_LOG_LINE_AFTER_SECS);
boolean again = false;
do {
try {
final DateTime now = UTCTime.now();
if (now.isAfter(nextDebugTime)) {
LOG.debug("waitUntilFinished: awaiting termination of executor for {} secs...",
(now.getMillis() - startTime.getMillis()) / 1000);
nextDebugTime = now.plusSeconds(ISSUE_WAITING_LOG_LINE_AFTER_SECS);
}
again = !executor.awaitTermination(
(timeout == 0) ? ISSUE_WAITING_LOG_LINE_AFTER_SECS : timeout,
(timeout == 0) ? TimeUnit.SECONDS : TimeUnit.MILLISECONDS);
} catch (final InterruptedException ignored) {
// Ignored.
}
}
while (again && (timeout == 0));
LOG.debug("waitUntilFinished: executor terminated (creating new one)");
/**
* Current pool is empty and done, get a new executor pool.
* Don't clear the exceptions list, as it is supposed to be the overall list for this
* WorkQueue instance (not fot the ThreadPoolExecutor instance).
*/
executor = createNewExecutor();
return exceptions.isEmpty();
}
/**
* Wait until the work pool finished executing all work load.
*
* @return False if exceptions were caught during executing workload packages.
*/
public boolean waitUntilFinished() {
return waitUntilFinished(0);
}
/**
* Check if there is workload available, or a thread is processing workload still.
*
* @return Workload is available, or a thread is busy processing last workload.
*/
public boolean isEmptyAndFinished() {
assert !executor.isShutdown();
assert Thread.currentThread().getId() == feederThread;
return executor.getQueue().isEmpty();
}
/**
* Add a specific exception to the work queue. This method may come in handy in the run() method of workload, to
* communicate specific exceptions to the WorkQueue during execution.
*
* @param exception Exception to be added.
*/
public void addException(@Nonnull final Exception exception) {
exceptions.add(exception);
}
/**
* Return any runtime exception that occurred in the threads. This method should only be called from the feeder
* thread.
*
* @return List of exceptions.
*/
@Nonnull
public List getExceptions() {
assert Thread.currentThread().getId() == feederThread;
return exceptions;
}
/**
* Create a new executor.
*
* @return Max work queue size.
*/
@Nonnull
private ThreadPoolExecutor createNewExecutor() {
final int nrCores = Runtime.getRuntime().availableProcessors();
final BlockingQueue queue = new LinkedBlockingQueue<>(this.maxQueueSize);
return new ThreadPoolExecutor(
Math.min(nrCores, MAX_THREADS_FOR_FULL_QUEUE), // Core pool.
MAX_THREADS_FOR_FULL_QUEUE, // Max. pool.
10, TimeUnit.SECONDS, // Keep-alive time.
queue); // Work queue.
}
private class RuntimeExceptionCatcher implements Runnable {
@Nonnull
private final Runnable runnable;
RuntimeExceptionCatcher(@Nonnull final Runnable runnable) {
assert runnable != null;
this.runnable = runnable;
}
@Override
public void run() {
try {
runnable.run();
} catch (final RuntimeException e) {
LOG.error("Runtime exception encoutered", e);
exceptions.add(e);
}
}
}
}