/*
* This file is part of LanternServer, licensed under the MIT License (MIT).
*
* Copyright (c) LanternPowered <https://www.lanternpowered.org>
* Copyright (c) SpongePowered <https://www.spongepowered.org>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the Software), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.lanternpowered.server.scheduler;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableList;
import org.spongepowered.api.plugin.PluginContainer;
import org.spongepowered.api.scheduler.SpongeExecutorService;
import org.spongepowered.api.scheduler.Task;
import java.util.List;
import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.Callable;
import java.util.concurrent.Delayed;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;
import javax.annotation.Nullable;
class TaskExecutorService extends AbstractExecutorService implements SpongeExecutorService {
private final Supplier<Task.Builder> taskBuilderProvider;
private final SchedulerBase scheduler;
private final PluginContainer plugin;
protected TaskExecutorService(Supplier<Task.Builder> taskBuilderProvider, SchedulerBase scheduler, PluginContainer plugin) {
this.taskBuilderProvider = taskBuilderProvider;
this.scheduler = scheduler;
this.plugin = plugin;
}
@Override
public void shutdown() {
// Since this class is delegating its work to SchedulerService
// and we have no way to stopping execution without keeping
// track of all the submitted tasks, it makes sense that
// this ExecutionService cannot be shut down.
// While it is technically possible to cancel all tasks for
// a plugin through the SchedulerService, we have no way to
// ensure those tasks were created through this interface.
}
@Override
public List<Runnable> shutdownNow() {
return ImmutableList.of();
}
@Override
public boolean isShutdown() {
return false;
}
@Override
public boolean isTerminated() {
return false;
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void execute(Runnable command) {
this.createTask(command).submit(this.plugin);
}
@Override
public SpongeFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
final FutureTask<?> runnable = new FutureTask<>(command, null);
final Task task = this.createTask(runnable)
.delay(delay, unit)
.submit(this.plugin);
return new SpongeTaskFuture<>(runnable, (ScheduledTask) task, this.scheduler);
}
@Override
public <V> SpongeFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
final FutureTask<V> runnable = new FutureTask<>(callable);
final Task task = this.createTask(runnable)
.delay(delay, unit)
.submit(this.plugin);
return new SpongeTaskFuture<>(runnable, (ScheduledTask) task, this.scheduler);
}
@Override
public SpongeFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
final RepeatableFutureTask<?> runnable = new RepeatableFutureTask<>(command);
final Task task = this.createTask(runnable)
.delay(initialDelay, unit)
.interval(period, unit)
.submit(this.plugin);
// A repeatable task needs to be able to cancel itself
runnable.setTask(task);
return new SpongeTaskFuture<>(runnable, (ScheduledTask) task, this.scheduler);
}
@Override
public SpongeFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
//Since we don't have full control over the execution, the contract needs to be a little broken
return this.scheduleAtFixedRate(command, initialDelay, delay, unit);
}
private Task.Builder createTask(Runnable command) {
return this.taskBuilderProvider.get().execute(command);
}
private static class SpongeTaskFuture<V> implements SpongeFuture<V> {
private final FutureTask<V> runnable;
private final ScheduledTask task;
private final SchedulerBase scheduler;
private SpongeTaskFuture(FutureTask<V> runnable, ScheduledTask task, SchedulerBase scheduler) {
this.runnable = runnable;
this.task = task;
this.scheduler = scheduler;
}
@Override
public Task getTask() {
return this.task;
}
@Override
public boolean isPeriodic() {
return this.task.getInterval() > 0;
}
@Override
public long getDelay(TimeUnit unit) {
// Since these tasks are scheduled through
// SchedulerExecutionService, they are
// always nanotime-based, not tick-based.
return unit.convert(
this.task.nextExecutionTimestamp() - this.scheduler.getTimestamp(this.task),
TimeUnit.NANOSECONDS
);
}
@SuppressWarnings("rawtypes")
@Override
public int compareTo(Delayed other) {
// Since getDelay may return different values for each call,
// this check is required to correctly implement Comparable
if (other == this) {
return 0;
}
// If we are considering other sponge tasks, we can order by
// their internal tasks
if (other instanceof SpongeTaskFuture) {
ScheduledTask otherTask = ((SpongeTaskFuture) other).task;
return ComparisonChain.start()
.compare(this.task.nextExecutionTimestamp(), otherTask.nextExecutionTimestamp())
.compare(this.task.getUniqueId(), otherTask.getUniqueId())
.result();
}
return Long.compare(this.getDelay(TimeUnit.NANOSECONDS), other.getDelay(TimeUnit.NANOSECONDS));
}
@Override
public void run() {
this.runnable.run();
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
this.task.cancel(); //Ensure Sponge is not going to try to run a cancelled task.
return this.runnable.cancel(mayInterruptIfRunning);
}
@Override
public boolean isCancelled() {
// It might be externally cancelled, the runnable would never
// run in that case
return this.runnable.isCancelled() || (this.task.getState() == ScheduledTask.ScheduledTaskState.CANCELED && !this.runnable.isDone());
}
@Override
public boolean isDone() {
return this.runnable.isDone();
}
@Override
public V get() throws InterruptedException, ExecutionException {
return this.runnable.get();
}
@Override
public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
return this.runnable.get(timeout, unit);
}
}
/**
* An extension of the JREs FutureTask that can be repeatedly executed,
* required for scheduling on an interval.
*/
private static class RepeatableFutureTask<V> extends FutureTask<V> {
@Nullable private Task owningTask = null;
protected RepeatableFutureTask(Runnable runnable) {
super(runnable, null);
}
protected void setTask(Task task) {
this.owningTask = task;
// Since it is set after being scheduled, it might have thrown
// an exception already
if (isDone() && !isCancelled()) {
this.owningTask.cancel();
}
}
@Override
protected void done() {
// A repeating task that is done but hasn't been cancelled has
// failed exceptionally. Following the contract of
// ScheduledExecutorService, this means the task has to be stopped.
if (!isCancelled() && this.owningTask != null) {
this.owningTask.cancel();
}
}
@Override
public void run() {
super.runAndReset();
}
}
}