Skip to content

Commit

Permalink
Merge pull request #4 from das-kaesebrot/feature/autopause-detection
Browse files Browse the repository at this point in the history
Implements new feature: tick drift detection
  • Loading branch information
das-kaesebrot authored Feb 23, 2023
2 parents 56313c4 + f56475f commit 525e5c9
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 143 deletions.
10 changes: 6 additions & 4 deletions src/main/java/eu/kaesebrot/dev/CronAnnouncerPlugin.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package eu.kaesebrot.dev;

import eu.kaesebrot.dev.classes.ScheduledMessage;
import eu.kaesebrot.dev.tasks.ScheduledMessageTaskQueuer;
import eu.kaesebrot.dev.tasks.ScheduledMessageTaskScheduler;
import eu.kaesebrot.dev.utils.ScheduleConfigParser;
import eu.kaesebrot.dev.utils.TickConverter;
import org.bukkit.plugin.java.JavaPlugin;
Expand All @@ -14,7 +14,7 @@
public class CronAnnouncerPlugin extends JavaPlugin {
private ScheduleConfigParser scheduleConfigParser;
private final TickConverter tickConverter = new TickConverter();
private BukkitTask subtaskScheduler;
private BukkitTask subtaskSchedulerTask;

Map<String, ScheduledMessage> scheduledMessages;

Expand Down Expand Up @@ -42,11 +42,13 @@ private void queueInitialScheduler() {
}

var queueAheadDuration = Duration.of(1, ChronoUnit.HOURS);
var queuer = new ScheduledMessageTaskQueuer(this, scheduledMessages, queueAheadDuration);
var subtaskScheduler = new ScheduledMessageTaskScheduler(this, scheduledMessages, queueAheadDuration);

getLogger().info("Queueing initial scheduler");

subtaskScheduler = queuer.runTaskTimer(this, 0L, tickConverter.durationToTicks(queueAheadDuration));
long pollingTicks = tickConverter.durationToTicks(Duration.ofSeconds(5));

subtaskSchedulerTask = subtaskScheduler.runTaskTimer(this, 0L, pollingTicks);
}

private void cancelAllTasks() {
Expand Down
7 changes: 3 additions & 4 deletions src/main/java/eu/kaesebrot/dev/classes/ScheduledMessage.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package eu.kaesebrot.dev.classes;

import com.cronutils.model.Cron;
import eu.kaesebrot.dev.utils.ScheduleConfigParser;

import java.util.Map;

public class ScheduledMessage {
private MessageType type;
private String text;
private Cron schedule;
private final MessageType type;
private final String text;
private final Cron schedule;

public ScheduledMessage(String text, Cron schedule, MessageType type) {
this.text = text;
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/eu/kaesebrot/dev/classes/ScheduledMessageTask.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package eu.kaesebrot.dev.classes;

import org.bukkit.scheduler.BukkitTask;

public record ScheduledMessageTask(BukkitTask task, long absoluteEndTicks) {

public void cancelIfEndTicksHavePassed(long absoluteTicksNow) {
if (absoluteTicksNow > absoluteEndTicks && !task.isCancelled()) {
task.cancel();
}
}
}
32 changes: 32 additions & 0 deletions src/main/java/eu/kaesebrot/dev/classes/TickReferencePoint.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package eu.kaesebrot.dev.classes;

import java.time.ZonedDateTime;

public class TickReferencePoint {
private long ticks;
private ZonedDateTime dateTime;

public TickReferencePoint() {
}

public TickReferencePoint(long ticks, ZonedDateTime dateTime) {
this.ticks = ticks;
this.dateTime = dateTime;
}

public long getTicks() {
return ticks;
}

public ZonedDateTime getDateTime() {
return dateTime;
}

public void setTicks(long ticks) {
this.ticks = ticks;
}

public void setDateTime(ZonedDateTime dateTime) {
this.dateTime = dateTime;
}
}
100 changes: 0 additions & 100 deletions src/main/java/eu/kaesebrot/dev/tasks/ScheduledMessageTaskQueuer.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package eu.kaesebrot.dev.tasks;

import eu.kaesebrot.dev.classes.ScheduledMessage;
import eu.kaesebrot.dev.classes.ScheduledMessageTask;
import eu.kaesebrot.dev.classes.TickReferencePoint;
import eu.kaesebrot.dev.utils.TickConverter;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;

import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class ScheduledMessageTaskScheduler extends BukkitRunnable
{
private final JavaPlugin plugin;
private final Map<String, ScheduledMessage> scheduledMessages;
private final Duration durationAhead;
private final List<ScheduledMessageTask> activeSubTasks = new ArrayList<>();
private final TickConverter tickConverter = new TickConverter();
private TickReferencePoint referencePoint;
private ZonedDateTime lastScheduleAheadUntil;

public ScheduledMessageTaskScheduler(JavaPlugin plugin, Map<String, ScheduledMessage> scheduledMessages, Duration durationAhead) {
this.plugin = plugin;
this.scheduledMessages = scheduledMessages;
this.durationAhead = durationAhead;
lastScheduleAheadUntil = ZonedDateTime.now();

updateReference();
}

@Override
public void run()
{
cleanUpPastRunningSubtasks();

var now = ZonedDateTime.now();

if (!ticksAreSync()) {
cleanUpAllRunningSubTasks();

// reset last scheduling end timeframe to ensure we re-queue all tasks again with the correct timestamps
lastScheduleAheadUntil = now;
}

// use lastScheduleAheadUntil if it's in the future, use now if last schedule was in the past
// if it's in the past, we're probably in the first ever run
var searchDateStart = (Duration.between(now, lastScheduleAheadUntil).isNegative() ? now : lastScheduleAheadUntil);
var searchDateEnd = now.plus(durationAhead);

for (var scheduledMessage: scheduledMessages.entrySet()) {
var message = scheduledMessage.getValue();

var nextRunTicksForMessage = tickConverter.getNextRunTicksUntil(scheduledMessage.getValue().getSchedule(),
searchDateStart, searchDateEnd);

if (nextRunTicksForMessage.isEmpty())
break;

plugin.getLogger().info(String.format("Scheduling new messages for %s=%s in time slot %s to %s",
scheduledMessage.getKey(), scheduledMessage.getValue().toString(), searchDateStart, searchDateEnd));

// guess if we can use a simple repeatable timer
// if present, we can, otherwise we have to queue single tasks for every iteration
var ticksRepeatableInterval = tickConverter.ticksRepeatableInterval(nextRunTicksForMessage);

if (ticksRepeatableInterval.isPresent()) {
var subTask = getRunnableForMessage(message).runTaskTimer(plugin, nextRunTicksForMessage.get(0), ticksRepeatableInterval.get());

activeSubTasks.add(new ScheduledMessageTask(subTask, getAbsoluteTicks() + nextRunTicksForMessage.get(nextRunTicksForMessage.size() - 1)));

plugin.getLogger().info(String.format("Scheduled repeatable message %s=%s running %s times every %s ticks, first run %s ticks from now",
scheduledMessage.getKey(), message, nextRunTicksForMessage.size(), ticksRepeatableInterval.get(), nextRunTicksForMessage.get(0)));
}
else
{
for (long nextRunTick: nextRunTicksForMessage)
{
var subTask = getRunnableForMessage(message).runTaskLater(plugin, nextRunTick);

var nextRunDateTime = tickConverter.ticksToDateTimeFromNow(nextRunTick);

activeSubTasks.add(new ScheduledMessageTask(subTask, getAbsoluteTicks() + nextRunTick));

plugin.getLogger().info(String.format("Scheduled single-run message %s=%s at %s (%s ticks from now)",
scheduledMessage.getKey(), message, nextRunDateTime.toString(), nextRunTick));
}
}
}

lastScheduleAheadUntil = searchDateEnd;
updateReference();
}

private void cleanUpAllRunningSubTasks() {
for (ScheduledMessageTask task: activeSubTasks)
{
task.task().cancel();
}

activeSubTasks.clear();
}

private void cleanUpPastRunningSubtasks() {
for (ScheduledMessageTask task: activeSubTasks)
{
task.cancelIfEndTicksHavePassed(getAbsoluteTicks() + TickConverter.getTicksPerSecond());
}

activeSubTasks.removeIf(s -> s.task().isCancelled());
}

private boolean ticksAreSync() {
var areSync = tickConverter.ticksAreSync(
referencePoint.getTicks(), referencePoint.getDateTime(),
getAbsoluteTicks(), ZonedDateTime.now());

if (!areSync) {
plugin.getLogger().warning("Detected sync loss between ticks and real time!");
updateReference();
}

return areSync;
}

private TickReferencePoint getReferencePointForNow() {
return new TickReferencePoint(getAbsoluteTicks(), ZonedDateTime.now());
}

// Update our reference point for checking if we're still synced to real time
private void updateReference() {
if (referencePoint == null) {
referencePoint = getReferencePointForNow();
return;
}

referencePoint.setTicks(getAbsoluteTicks());
referencePoint.setDateTime(ZonedDateTime.now());
}

private BukkitRunnable getRunnableForMessage(ScheduledMessage message) {
return switch (message.getType()) {
case TITLE -> new TitleTask(plugin, message.getText());
case BROADCAST -> new BroadcastTask(plugin, message.getText());
default -> throw new IllegalArgumentException("Illegal message type provided");
};
}

private long getAbsoluteTicks() {
return plugin.getServer().getWorlds().get(0).getFullTime();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import java.util.Map;

import static com.cronutils.model.CronType.CRON4J;
import static org.bukkit.plugin.java.JavaPlugin.getPlugin;

public class ScheduleConfigParser
{
Expand All @@ -21,7 +20,7 @@ public class ScheduleConfigParser
private String KEY_TYPE = "type";
private final CronParser parser;

private JavaPlugin plugin;
private final JavaPlugin plugin;

public ScheduleConfigParser(JavaPlugin plugin) {
this.plugin = plugin;
Expand All @@ -35,6 +34,7 @@ public Map<String, ScheduledMessage> parseConfig()
// do nothing if root is empty
if (!this.plugin.getConfig().contains(KEY_ROOT)) return parsedMessages;

//noinspection DataFlowIssue
var subKeys = this.plugin.getConfig().getConfigurationSection(KEY_ROOT).getValues(false);

for (Map.Entry<String, Object> entry : subKeys.entrySet()) {
Expand Down
Loading

0 comments on commit 525e5c9

Please sign in to comment.