/*
* 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.Maps;
import com.google.common.collect.Sets;
import org.lanternpowered.server.game.Lantern;
import org.spongepowered.api.plugin.PluginContainer;
import org.spongepowered.api.scheduler.Task;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
abstract class SchedulerBase {
// The simple queue of all pending (and running) ScheduledTasks
private final Map<UUID, ScheduledTask> taskMap = Maps.newConcurrentMap();
private long sequenceNumber = 0L;
private final String taskNameFmt;
protected SchedulerBase(ScheduledTask.TaskSynchronicity type) {
this.taskNameFmt = "%s-" + (type == ScheduledTask.TaskSynchronicity.SYNCHRONOUS ? "S" : "A") + "-%d";
}
protected String nextName(PluginContainer plugin) {
return String.format(this.taskNameFmt, plugin.getId(), this.sequenceNumber++);
}
/**
* Gets the timestamp to update the timestamp of a task. This method is task
* sensitive to support different timestamp types i.e. real time and ticks.
*
* <p>Subtracting the result of this method from a previously obtained
* result should become a representation of the time that has passed
* between those calls.</p>
*
* @param task The task
* @return Timestamp for the task
*/
protected long getTimestamp(ScheduledTask task) {
// Supports wall clock time by default
return System.nanoTime();
}
/**
* Adds the task to the task map, will attempt to process the task on the
* next call to {@link #runTick}.
*
* @param task The task to add
*/
protected void addTask(ScheduledTask task) {
task.setTimestamp(this.getTimestamp(task));
this.taskMap.put(task.getUniqueId(), task);
}
/**
* Removes the task from the task map.
*
* @param task The task to remove
*/
protected void removeTask(ScheduledTask task) {
this.taskMap.remove(task.getUniqueId());
}
protected Optional<Task> getTask(UUID id) {
return Optional.ofNullable(this.taskMap.get(id));
}
protected Set<Task> getScheduledTasks() {
synchronized (this.taskMap) {
return Sets.newHashSet(this.taskMap.values());
}
}
/**
* Process all tasks in the map.
*/
protected final void runTick() {
this.preTick();
try {
this.taskMap.values().forEach(this::processTask);
this.postTick();
} finally {
this.finallyPostTick();
}
}
/**
* Fired when the scheduler begins to tick, before any tasks are processed.
*/
protected void preTick() {
}
/**
* Fired when the scheduler has processed all tasks.
*/
protected void postTick() {
}
/**
* Fired after tasks have attempted to be processed, in a finally block to
* guarantee execution regardless of any error when processing a task.
*/
protected void finallyPostTick() {
}
/**
* Processes the task.
*
* @param task The task to process
*/
protected void processTask(ScheduledTask task) {
// If the task is now slated to be cancelled, we just remove it as if it
// no longer exists.
if (task.getState() == ScheduledTask.ScheduledTaskState.CANCELED) {
this.removeTask(task);
return;
}
long threshold = Long.MAX_VALUE;
// Figure out if we start a delayed Task after threshold ticks or, start
// it after the interval (period) of the repeating task parameter.
if (task.getState() == ScheduledTask.ScheduledTaskState.WAITING) {
threshold = task.offset;
} else if (task.getState() == ScheduledTask.ScheduledTaskState.RUNNING) {
threshold = task.period;
}
// This moment is 'now'
long now = this.getTimestamp(task);
// So, if the current time minus the timestamp of the task is greater
// than the delay to wait before starting the task, then start the task.
// Repeating tasks get a reset-timestamp each time they are set RUNNING
// If the task has a period of 0 (zero) this task will not repeat, and
// is removed after we start it.
if (threshold <= (now - task.getTimestamp())) {
task.setState(ScheduledTask.ScheduledTaskState.SWITCHING);
task.setTimestamp(this.getTimestamp(task));
startTask(task);
// If task is one time shot, remove it from the map.
if (task.period == 0L) {
this.removeTask(task);
}
}
}
/**
* Begin the execution of a task. Exceptions are caught and logged.
*
* @param task The task to start
*/
protected void startTask(final ScheduledTask task) {
this.executeTaskRunnable(() -> {
task.setState(ScheduledTask.ScheduledTaskState.RUNNING);
try {
task.getConsumer().accept(task);
} catch (Throwable t) {
Lantern.getLogger().error("The Scheduler tried to run the task {} owned by {}, but an error occured.",
task.getName(), task.getOwner(), t);
}
});
}
/**
* Actually run the runnable of a task.
*
* @param runnable The runnable to run
*/
protected abstract void executeTaskRunnable(Runnable runnable);
}