2023-09-20 08:11:49 +00:00
|
|
|
package nu.marginalia.util;
|
2023-07-28 16:15:16 +00:00
|
|
|
|
|
|
|
import org.slf4j.Logger;
|
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
|
2023-10-26 10:49:28 +00:00
|
|
|
import java.time.Duration;
|
2023-07-28 16:15:16 +00:00
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.List;
|
2023-09-20 08:11:49 +00:00
|
|
|
import java.util.concurrent.ArrayBlockingQueue;
|
|
|
|
import java.util.concurrent.BlockingQueue;
|
2023-07-28 16:15:16 +00:00
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
|
|
|
2023-09-20 08:11:49 +00:00
|
|
|
/** A dead simple thread pool implementation that will block the caller
|
|
|
|
* when it is not able to perform a task. This is desirable in batch
|
|
|
|
* processing workloads.
|
2023-07-28 16:15:16 +00:00
|
|
|
*/
|
2023-09-20 08:11:49 +00:00
|
|
|
public class SimpleBlockingThreadPool {
|
2023-07-28 16:15:16 +00:00
|
|
|
private final List<Thread> workers = new ArrayList<>();
|
2023-09-20 08:11:49 +00:00
|
|
|
private final BlockingQueue<Task> tasks;
|
2023-07-28 16:15:16 +00:00
|
|
|
private volatile boolean shutDown = false;
|
|
|
|
private final AtomicInteger taskCount = new AtomicInteger(0);
|
2023-09-20 08:11:49 +00:00
|
|
|
private final Logger logger = LoggerFactory.getLogger(SimpleBlockingThreadPool.class);
|
2023-07-28 16:15:16 +00:00
|
|
|
|
2023-09-20 08:11:49 +00:00
|
|
|
public SimpleBlockingThreadPool(String name, int poolSize, int queueSize) {
|
|
|
|
tasks = new ArrayBlockingQueue<>(queueSize);
|
2023-07-28 16:15:16 +00:00
|
|
|
|
|
|
|
for (int i = 0; i < poolSize; i++) {
|
2023-09-20 08:11:49 +00:00
|
|
|
Thread worker = new Thread(this::worker, name + "[" + i + "]");
|
2023-07-28 16:15:16 +00:00
|
|
|
worker.setDaemon(true);
|
|
|
|
worker.start();
|
|
|
|
workers.add(worker);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2023-07-30 14:53:39 +00:00
|
|
|
public void submit(Task task) throws InterruptedException {
|
|
|
|
tasks.put(task);
|
2023-07-28 16:15:16 +00:00
|
|
|
}
|
2023-09-16 16:14:47 +00:00
|
|
|
public void submitQuietly(Task task) {
|
|
|
|
try {
|
|
|
|
tasks.put(task);
|
|
|
|
}
|
|
|
|
catch (InterruptedException ex) {
|
|
|
|
throw new RuntimeException(ex);
|
|
|
|
}
|
|
|
|
}
|
2023-07-28 16:15:16 +00:00
|
|
|
public void shutDown() {
|
|
|
|
this.shutDown = true;
|
|
|
|
}
|
|
|
|
|
2023-10-26 10:49:28 +00:00
|
|
|
public void shutDownNow() throws InterruptedException {
|
2023-07-28 16:15:16 +00:00
|
|
|
this.shutDown = true;
|
2023-10-14 10:07:40 +00:00
|
|
|
tasks.clear();
|
2023-07-28 16:15:16 +00:00
|
|
|
for (Thread worker : workers) {
|
|
|
|
worker.interrupt();
|
|
|
|
}
|
2023-10-26 10:49:28 +00:00
|
|
|
for (Thread worker : workers) {
|
|
|
|
worker.join(Duration.ofMinutes(5));
|
|
|
|
}
|
2023-07-28 16:15:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private void worker() {
|
2023-10-19 11:22:52 +00:00
|
|
|
while (!(tasks.isEmpty() && shutDown)) {
|
2023-07-28 16:15:16 +00:00
|
|
|
try {
|
2023-07-30 14:53:39 +00:00
|
|
|
Task task = tasks.poll(1, TimeUnit.SECONDS);
|
2023-07-28 16:15:16 +00:00
|
|
|
if (task == null) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
taskCount.incrementAndGet();
|
|
|
|
task.run();
|
|
|
|
}
|
|
|
|
catch (Exception ex) {
|
|
|
|
logger.warn("Error executing task", ex);
|
|
|
|
}
|
|
|
|
finally {
|
|
|
|
taskCount.decrementAndGet();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
catch (InterruptedException ex) {
|
|
|
|
logger.warn("Thread pool worker interrupted", ex);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-07-29 17:19:09 +00:00
|
|
|
/** Wait for all tasks to complete up to the specified timeout,
|
|
|
|
* then return true if all tasks completed, false otherwise.
|
|
|
|
*/
|
|
|
|
public boolean awaitTermination(int i, TimeUnit timeUnit) throws InterruptedException {
|
2023-07-28 16:15:16 +00:00
|
|
|
final long start = System.currentTimeMillis();
|
|
|
|
final long deadline = start + timeUnit.toMillis(i);
|
|
|
|
|
2023-10-14 10:07:40 +00:00
|
|
|
// Wait for termination
|
2023-07-28 16:15:16 +00:00
|
|
|
for (var thread : workers) {
|
|
|
|
if (!thread.isAlive())
|
|
|
|
continue;
|
|
|
|
|
|
|
|
long timeRemaining = deadline - System.currentTimeMillis();
|
|
|
|
if (timeRemaining <= 0)
|
|
|
|
return false;
|
|
|
|
|
2023-07-29 17:19:09 +00:00
|
|
|
thread.join(timeRemaining);
|
|
|
|
if (thread.isAlive())
|
2023-07-28 16:15:16 +00:00
|
|
|
return false;
|
2023-07-29 17:19:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Doublecheck the bookkeeping so we didn't mess up. This may mean you have to Ctrl+C the process
|
|
|
|
// if you see this warning forever, but for the crawler this is preferable to terminating early
|
|
|
|
// and missing tasks. (maybe some cosmic ray or OOM condition or X-Files baddie of the week killed a
|
|
|
|
// thread so hard and it didn't invoke finally and didn't decrement the task count)
|
|
|
|
|
|
|
|
int activeCount = getActiveCount();
|
|
|
|
if (activeCount != 0) {
|
|
|
|
logger.warn("Thread pool terminated with {} active threads(?!) -- check what's going on with jstack and kill manually", activeCount);
|
|
|
|
return false;
|
2023-07-28 16:15:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public int getActiveCount() {
|
|
|
|
return taskCount.get();
|
|
|
|
}
|
|
|
|
|
2023-09-21 10:47:41 +00:00
|
|
|
public boolean isTerminated() {
|
|
|
|
return shutDown && getActiveCount() == 0;
|
|
|
|
}
|
|
|
|
|
2023-07-30 14:53:39 +00:00
|
|
|
public interface Task {
|
|
|
|
void run() throws Exception;
|
|
|
|
}
|
2023-09-20 08:11:49 +00:00
|
|
|
|
2023-07-28 16:15:16 +00:00
|
|
|
}
|