ru.fix.stdlib.concurrency.threads.ReschedulableScheduler Maven / Gradle / Ivy
package ru.fix.stdlib.concurrency.threads;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ru.fix.aggregating.profiler.Profiler;
import ru.fix.dynamic.property.api.DynamicProperty;
import ru.fix.dynamic.property.api.PropertySubscription;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* Wrapper under runnable task. Save and check schedule value. If value was changed, task will be rescheduled.
*/
public class ReschedulableScheduler implements AutoCloseable {
private static final long DEFAULT_START_DELAY = 0L;
private final ScheduledExecutorService executorService;
private final Set activeTasks;
private final Profiler profiler;
private volatile boolean isShutdown = false;
private final Logger log;
private final String scheduledTasksIndicatorName;
/**
* ReschedulableScheduler based on {@link ProfiledScheduledThreadPoolExecutor} created with given parameters
*
* @param poolName will be used as
*/
public ReschedulableScheduler(String poolName, DynamicProperty maxPoolSize, Profiler profiler) {
log = LoggerFactory.getLogger(ReschedulableScheduler.class.getName() + "." + poolName);
this.executorService = new ProfiledScheduledThreadPoolExecutor(poolName, maxPoolSize, profiler);
this.activeTasks = ConcurrentHashMap.newKeySet();
this.profiler = profiler;
scheduledTasksIndicatorName = "scheduled.pool." + poolName + ".tasks.count";
profiler.attachIndicator(scheduledTasksIndicatorName, () -> (long) activeTasks.size());
}
private void detachIndicators() {
profiler.detachIndicator(scheduledTasksIndicatorName);
}
/**
* change execution by schedule type
*
* @return result task from executionService
*/
public ScheduledFuture> schedule(DynamicProperty scheduleSupplier,
DynamicProperty startDelay,
Runnable task) {
if (isShutdown) {
throw new IllegalStateException("ReschedulableScheduler is shutdown and can not schedule new task." +
" Task: " + task);
}
SelfSchedulableTaskWrapper taskWrapper = new SelfSchedulableTaskWrapper(
scheduleSupplier,
startDelay,
task,
executorService,
activeTasks::remove,
log
);
activeTasks.add(taskWrapper);
return taskWrapper.launch();
}
public ScheduledFuture> schedule(DynamicProperty scheduleSupplier, long startDelay, Runnable task) {
return schedule(scheduleSupplier, DynamicProperty.of(startDelay), task);
}
/**
* change execution by schedule type with start delay 0
*
* @return result task from executionService
*/
public ScheduledFuture> schedule(DynamicProperty schedule, Runnable task) {
return schedule(schedule, DEFAULT_START_DELAY, task);
}
/**
* shutdown scheduler's executorService
*
* shorter version of {@code getExecutorService().shutdown()}
*/
public void shutdown() {
cancelAllTasks(false);
executorService.shutdown();
detachIndicators();
isShutdown = true;
}
/**
* shutdownNow scheduler's executorService
*
* shorter version of {@code getExecutorService().shutdownNow()}
*/
public void shutdownNow() {
cancelAllTasks(true);
executorService.shutdownNow();
detachIndicators();
isShutdown = true;
}
private void cancelAllTasks(boolean mayInterruptIfRunning) {
for (SelfSchedulableTaskWrapper task : activeTasks.toArray(new SelfSchedulableTaskWrapper[0])) {
task.cancel(mayInterruptIfRunning);
}
}
/**
* Blocks until all tasks have completed execution after a shutdown
* request, or the timeout occurs, or the current thread is
* interrupted, whichever happens first.
*/
public boolean awaitTermination(long timeout, TimeUnit timeUnit) throws InterruptedException {
return executorService.awaitTermination(timeout, timeUnit);
}
private static class SelfSchedulableTaskWrapper implements Runnable {
private final Logger log;
private ScheduleSettingsSnapshot prevScheduleSettingsSnapshot;
private final DynamicProperty schedule;
private PropertySubscription scheduleSubscription;
private PropertySubscription initialDelaySubscription;
private final DynamicProperty startDelay;
private ScheduledFuture> scheduledFuture;
private final Runnable task;
private final ReschedulableSchedullerFuture reschedulableFuture =
new ReschedulableSchedullerFuture(this);
private final Consumer cancelHandler;
private final ScheduledExecutorService executorService;
private volatile ScheduleSettings settings;
private volatile long lastExecutedTs = 0L;
private final AtomicBoolean taskIsRunning = new AtomicBoolean(false);
public SelfSchedulableTaskWrapper(DynamicProperty schedule,
DynamicProperty startDelay,
Runnable task,
ScheduledExecutorService executorService,
Consumer cancelHandler,
Logger log) {
this.schedule = schedule;
this.startDelay = startDelay;
this.task = task;
this.executorService = executorService;
this.cancelHandler = cancelHandler;
this.log = log;
}
@Override
@SuppressWarnings("squid:S1181")
public void run() {
ScheduledFuture> scheduledFuture;
synchronized (this) {
scheduledFuture = this.scheduledFuture;
}
//Preventing concurrent task launch for the case when
// previously launched task still working,
// schedule changed and triggered new scheduled task to launch
if (!taskIsRunning.compareAndSet(false, true)) {
log.trace("Preventing concurrent task launch; scheduledFuture={} with hash={}",
scheduledFuture, System.identityHashCode(scheduledFuture));
return;
}
try {
ScheduleSettings currSettings = this.settings;
if (currSettings.type == Schedule.Type.RATE) {
//
// If fixed rate tasks take more time that given rate
// then next invocation of task starts to happen immediately
// and total invocation rate will exceed rate limit
//
// Suppose standard java scheduled executor configured with fixed rate once in 10 sec.
// If regular task takes 2 sec to execute then actual task execution rate will be as configured:
// taskId, start time, end time:
// 1: 0-2
// 2: 10-12
// 3: 20-22
// 4: 30-32
// 5: 40-42
// 6: 50-52
//
// If first task will take more time, e.g. 33 seconds.
// Then standard java schedule will remember how many scheduled task it didn't run.
// And it will try to launch skipped tasks immediately as first opportunity occurred.
// This will lead to wrong actual task launching rate:
// taskId, start time, end time:
// 1: 0-33
// 2: 33-35
// 3: 35-37
// 4: 37-39
// 5: 40-42
// 6: 50-52
// In this case tasks 2,3,4 are running with wrong rate.
// To fix that we will skip all task invocations that occurred too earlie.
//
// skip wrong invocations
long now = System.currentTimeMillis();
if (now < lastExecutedTs + currSettings.periodValue - currSettings.safeDelay()) {
log.trace("skip wrong invocation; now={}, lastExecutedTs={}, currSettings={}; scheduledFuture={} with hash={}",
now, lastExecutedTs, currSettings,
scheduledFuture, System.identityHashCode(scheduledFuture));
return;
}
lastExecutedTs = now;
}
log.trace("running task; scheduledFuture={} with hash={}",
scheduledFuture, System.identityHashCode(scheduledFuture));
task.run();
} catch (Throwable exc) {
log.error("ReschedulableScheduler task failed due to: " + exc.getMessage(), exc);
} finally {
log.trace("Set taskIsRunning flag to false; scheduledFuture={} with hash={}",
scheduledFuture, System.identityHashCode(scheduledFuture));
taskIsRunning.compareAndSet(true, false);
checkPreviousScheduleAndRestartTask(new ScheduleSettingsSnapshot(schedule.get(), startDelay.get()));
}
}
private synchronized void checkPreviousScheduleAndRestartTask(ScheduleSettingsSnapshot scheduleSettingsSnapshot) {
ScheduledFuture> curScheduledFuture = scheduledFuture;
if (curScheduledFuture != null && curScheduledFuture.isCancelled()) {
return;
}
ScheduleSettingsSnapshot settingsSnapshot = prevScheduleSettingsSnapshot;
if (settingsSnapshot != null && settingsSnapshot.equals(scheduleSettingsSnapshot)) {
return;
}
this.prevScheduleSettingsSnapshot = scheduleSettingsSnapshot;
if (curScheduledFuture != null) {
log.trace("checkPreviousScheduleAndRestartTask cancelling scheduledFuture {} with hash={}",
curScheduledFuture, System.identityHashCode(curScheduledFuture));
curScheduledFuture.cancel(false);
}
this.scheduledFuture = schedule(
this,
scheduleSettingsSnapshot.schedule,
scheduleSettingsSnapshot.startDelay
);
log.trace("checkPreviousScheduleAndRestartTask new scheduledFuture {} with hash={} is scheduled",
curScheduledFuture, System.identityHashCode(curScheduledFuture));
}
public synchronized ScheduledFuture> launch() {
this.scheduleSubscription = this.schedule
.createSubscription()
.setAndCallListener((oldVal, newVal) ->
checkPreviousScheduleAndRestartTask(new ScheduleSettingsSnapshot(newVal, startDelay.get()))
);
this.initialDelaySubscription = startDelay
.createSubscription()
.setAndCallListener((oldValue, newValue) ->
checkPreviousScheduleAndRestartTask(new ScheduleSettingsSnapshot(schedule.get(), newValue))
);
log.trace("scheduledFuture={} with hash={} is launched",
scheduledFuture, System.identityHashCode(scheduledFuture));
return reschedulableFuture;
}
private synchronized ScheduledFuture> schedule(SelfSchedulableTaskWrapper taskWrapper,
Schedule schedule, long startDelay) {
long periodValue = schedule.getValue();
Schedule.Type type = schedule.getType();
settings = new ScheduleSettings(type, periodValue);
switch (type) {
case RATE:
return executorService.scheduleAtFixedRate(taskWrapper, startDelay,
periodValue, TimeUnit.MILLISECONDS);
case DELAY:
return executorService.scheduleWithFixedDelay(taskWrapper, startDelay,
periodValue, TimeUnit.MILLISECONDS);
default:
throw new IllegalArgumentException("Invalid schedule type: " + type);
}
}
synchronized T accessScheduledFuture(Function, T> accessor) {
return accessor.apply(scheduledFuture);
}
public synchronized void cancel(boolean mayInterruptIfRunning) {
log.trace("cancelling scheduledFuture {} with hash={}",
scheduledFuture, System.identityHashCode(scheduledFuture));
scheduleSubscription.close();
initialDelaySubscription.close();
scheduledFuture.cancel(mayInterruptIfRunning);
cancelHandler.accept(this);
}
}
private static class ReschedulableSchedullerFuture implements ScheduledFuture