com.metaeffekt.mirror.concurrency.ScheduledDelayedThreadPoolExecutor Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2021-2024 the original author or authors.
*
* 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.metaeffekt.mirror.concurrency;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public class ScheduledDelayedThreadPoolExecutor {
private final static Logger LOG = LoggerFactory.getLogger(ScheduledDelayedThreadPoolExecutor.class);
protected final static int LOG_PROGRESS_EVERY_PERCENT = 25;
private ScheduledThreadPoolExecutor executor;
private final BlockingQueue backlog;
private final AtomicInteger remainingScheduledTasks = new AtomicInteger(0);
private final AtomicInteger maxTaskListSize = new AtomicInteger(0);
private final AtomicLong batchStartTime = new AtomicLong();
private final AtomicLong currentBatchEndTime = new AtomicLong();
private final AtomicLong lastLoggedProgressPercent = new AtomicLong();
private final List> exceptions = Collections.synchronizedList(new ArrayList<>());
private long delay;
private boolean logProgress = true;
private int logEveryPercent = LOG_PROGRESS_EVERY_PERCENT;
public ScheduledDelayedThreadPoolExecutor(int size, long delay) {
this.executor = this.createExecutor(size);
this.backlog = new LinkedBlockingQueue<>();
this.setDelay(delay);
this.currentBatchEndTime.set(System.currentTimeMillis());
}
public void setSize(int size) {
if (size <= 0) throw new IllegalArgumentException("Size must be greater than 0");
final int maxThreads = Math.max(1, Math.min(size, Runtime.getRuntime().availableProcessors() - 1));
this.executor.setCorePoolSize(maxThreads);
}
public void setDelay(long delay) {
if (delay < 0) throw new IllegalArgumentException("Delay must be non-negative");
this.delay = delay;
}
public void setLogProgress(boolean logProgress) {
this.logProgress = logProgress;
}
public void setLogEveryPercent(int logEveryPercent) {
this.logEveryPercent = logEveryPercent;
}
public void submit(ThrowingRunnable task) {
if (task == null) throw new IllegalArgumentException("Task must not be null");
if (task instanceof Thread) {
LOG.warn("Thread [{}] submitted to executor. Use Runnable instead", ((Thread) task).getName());
}
this.backlog.offer(task);
}
public void start() {
final long startTime = System.currentTimeMillis();
batchStartTime.set(startTime);
final long timeRemainingUntilDone = currentBatchEndTime.get() - startTime;
long delay;
if (timeRemainingUntilDone > 0) {
delay = timeRemainingUntilDone + this.delay;
} else {
delay = this.delay;
}
while (!backlog.isEmpty()) {
executor.schedule(backlog.poll(), delay, TimeUnit.MILLISECONDS);
delay += this.delay;
remainingScheduledTasks.incrementAndGet();
}
currentBatchEndTime.set(System.currentTimeMillis() + delay - this.delay);
maxTaskListSize.set(Math.max(maxTaskListSize.get(), remainingScheduledTasks.get()));
}
public void join() throws InterruptedException {
executor.shutdown();
while (!executor.awaitTermination(5, TimeUnit.MINUTES)) {
LOG.info("Waiting for executor to terminate");
}
final int corePoolSize = executor.getCorePoolSize();
executor = this.createExecutor(corePoolSize);
remainingScheduledTasks.set(0);
maxTaskListSize.set(0);
lastLoggedProgressPercent.set(0);
currentBatchEndTime.set(System.currentTimeMillis());
LOG.info("Executor terminated");
if (!exceptions.isEmpty()) {
throw new RuntimeException(exceptions.size() + " exceptions occurred during execution. See log for details.");
}
}
public boolean isRunning() {
if (executor.isTerminated()) {
return false;
}
return remainingScheduledTasks.get() > 0;
}
private int calculateRemainingPercent() {
final double remainingTasks = remainingScheduledTasks.get();
final double maxTasks = maxTaskListSize.get();
if (maxTasks == 0) return 0;
return (int) (100 - (remainingTasks / (maxTasks / 100)));
}
private long calculateEstimatedTimeRemaining() {
final long completedTasks = maxTaskListSize.get() - remainingScheduledTasks.get();
if (completedTasks == 0) return 0;
final long elapsedTime = System.currentTimeMillis() - batchStartTime.get();
final long averageTaskTime = elapsedTime / completedTasks;
return averageTaskTime * remainingScheduledTasks.get();
}
private boolean shouldProgressBeLogged() {
if (!logProgress) return false;
final long lastLoggedProgress = this.lastLoggedProgressPercent.get();
final long currentProgress = calculateRemainingPercent();
if (currentProgress == 0) return false;
if (currentProgress - lastLoggedProgress >= this.logEveryPercent) {
this.lastLoggedProgressPercent.set(currentProgress);
return true;
}
return false;
}
private ScheduledThreadPoolExecutor createExecutor(int size) {
return new ScheduledThreadPoolExecutor(size) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
remainingScheduledTasks.decrementAndGet();
if (shouldProgressBeLogged()) {
final int remainingPercent = calculateRemainingPercent();
// TODO: properly calculate time remaining, then show it in the log message
final long estimatedTimeRemaining = calculateEstimatedTimeRemaining();
final long estimatedTimeRemainingSecs = TimeUnit.MILLISECONDS.toSeconds(estimatedTimeRemaining);
LOG.info("Started [{} / {}] tasks [{} %]",
maxTaskListSize.get() - remainingScheduledTasks.get(),
maxTaskListSize.get(),
remainingPercent);
}
if (t == null && r instanceof Future>) {
try {
Future> future = (Future>) r;
if (future.isDone()) {
future.get();
}
} catch (CancellationException ce) {
t = ce;
} catch (ExecutionException ee) {
t = ee.getCause();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // propagate interrupt
}
}
if (t != null) {
if (t instanceof ThrowingRunnableException) {
t = t.getCause();
}
exceptions.add(Pair.of(maxTaskListSize.get() - remainingScheduledTasks.get(), t));
throw new RuntimeException("Exception in scheduled task at ~" + (maxTaskListSize.get() - remainingScheduledTasks.get()), t);
}
}
};
}
private static class ThrowingRunnableException extends RuntimeException {
public ThrowingRunnableException(String message) {
super(message);
}
public ThrowingRunnableException(String message, Throwable cause) {
super(message, cause);
}
public ThrowingRunnableException(Throwable cause) {
super(cause);
}
}
public interface ThrowingRunnable extends Runnable {
@Override
default void run() {
try {
runThrows();
} catch (Exception e) {
throw new ThrowingRunnableException(e);
}
}
void runThrows() throws Exception;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy