net.intelie.pipes.time.OrderedScheduler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pipes-api Show documentation
Show all versions of pipes-api Show documentation
Intelie Pipes' API classes and interfaces
package net.intelie.pipes.time;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class OrderedScheduler implements ClockScheduler {
private final List contexts = new ArrayList<>();
private final ScheduledExecutorService executor;
private final Clock clock;
private final TaskRunner runner;
private ScheduledFuture future;
private List actionsBuffer;
private volatile long expectedNext = Long.MAX_VALUE;
/**
* When true, indicates that executeNext() is running but has dropped the lock.
*/
private boolean executingTasks;
public OrderedScheduler() {
this(Executors.newSingleThreadScheduledExecutor());
}
public OrderedScheduler(ScheduledExecutorService executor) {
this(executor, new SystemClock());
}
public OrderedScheduler(ScheduledExecutorService executor, Clock clock) {
this(executor, clock, null);
}
public OrderedScheduler(ScheduledExecutorService executor, Clock clock, TaskRunner runner) {
this.runner = runner != null ? runner : new TaskRunner.Default();
this.executor = executor;
this.clock = clock;
}
// This function must always be called within a synchronized block.
private void rescheduleNext() {
// If executingTasks is true, whoever sets it to false will complete the future and call scheduleNext() for us.
if (executingTasks) return;
// Optimistically attempt to prevent the thread from running. If already running, the check for expectedNext
// will make it exit.
if (future != null)
future.cancel(false);
// Create a new future for the correct timestamp.
scheduleNext();
}
public synchronized void cancelAll() {
while (!contexts.isEmpty()) {
contexts.get(0).cancelAll();
}
}
// This function must always be called within a synchronized block.
private void scheduleNext() {
assert Thread.holdsLock(this);
long now = clock.now();
final long next = this.expectedNext = nextTaskTimestamp();
if (next == Long.MAX_VALUE) return;
future = executor.schedule(() -> executeNext(next), Math.max(next - now, 0), TimeUnit.MILLISECONDS);
}
private long nextTaskTimestamp() {
long next = Long.MAX_VALUE;
for (MyContext context : contexts) {
next = Math.min(next, context.nextTaskTimestamp());
}
return next;
}
/**
* Returns the timestamp of the currently executing task, or of the next task if none is currently executing.
*/
public long currentTaskTimestamp() {
// The expectedNext variable is updated at the end of the task execution, or outside it when rescheduling.
return expectedNext;
}
private void executeNext(long next) {
List bufferCopy;
synchronized (this) {
// There was a race condition with a reschedule, and more than one copy of this code is running.
if (executingTasks || next != expectedNext) return;
actionsBuffer = new ArrayList<>();
for (MyContext context : contexts) {
context.advanceTo(next);
}
bufferCopy = actionsBuffer;
actionsBuffer = null;
// We're about to drop the lock
executingTasks = true;
}
try {
// Run outside the lock
runner.onTasks(next, bufferCopy);
assert !Thread.holdsLock(this);
} finally {
synchronized (this) {
// The lock has been reacquired
executingTasks = false;
scheduleNext();
}
}
}
@Override
public SchedulerContext newContext() {
return new MyContext();
}
@Override
public long now() {
return clock.now();
}
private class MyContext implements SchedulerContext {
private final ManualSchedulerContext context;
private MyContext() {
this.context = new ManualSchedulerContext((timestamp, actions) -> actionsBuffer.addAll(actions));
}
@Override
public TaskHandle schedule(Period period, Task runnable) {
TaskHandle result;
synchronized (OrderedScheduler.this) {
final TaskHandle handle = context.schedule(period, runnable);
if (context.started())
rescheduleNext();
result = new TaskHandle() {
@Override
public long nextTimestamp() {
return handle.nextTimestamp();
}
@Override
public void cancel() {
synchronized (OrderedScheduler.this) {
handle.cancel();
if (context.started())
rescheduleNext();
}
}
};
}
return result;
}
@Override
public long now() {
return context.now();
}
@Override
public boolean started() {
return context.started();
}
@Override
public long startTime() {
return context.startTime();
}
@Override
public void start() {
synchronized (OrderedScheduler.this) {
context.advanceTo(clock.now());
context.start();
contexts.add(this);
rescheduleNext();
}
}
@Override
public void cancelAll() {
synchronized (OrderedScheduler.this) {
contexts.remove(this);
context.cancelAll();
rescheduleNext();
}
}
public long nextTaskTimestamp() {
return context.nextTaskTimestamp();
}
public void advanceTo(long next) {
context.advanceTo(next);
}
}
}