package tc.oc.commons.core.restart;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import tc.oc.api.docs.Server;
import tc.oc.api.docs.virtual.ServerDoc;
import tc.oc.api.minecraft.MinecraftService;
import tc.oc.api.minecraft.servers.LocalServerReconfigureEvent;
import tc.oc.api.minecraft.users.OnlinePlayers;
import tc.oc.commons.core.exception.NamedThreadFactory;
import tc.oc.commons.core.logging.Loggers;
import tc.oc.commons.core.plugin.PluginFacet;
import tc.oc.commons.core.util.Comparables;
import tc.oc.minecraft.api.scheduler.Tickable;
import tc.oc.minecraft.api.server.LocalServer;
/**
* Manages restarting logic for all plugins.
*
* Also monitors uptime and memory use and automatically requests a restart if needed.
*/
@Singleton
public class RestartManager implements PluginFacet, Tickable {
private final Logger logger;
private final LocalServer minecraftServer;
private final MinecraftService minecraftService;
private final Server localServer;
private final RestartConfiguration config;
private final EventBus eventBus;
private final OnlinePlayers onlinePlayers;
private final NamedThreadFactory threads;
private final Instant startTime;
private @Nullable RequestRestartEvent currentRequest;
private @Nullable ScheduledExecutorService timer;
@Inject RestartManager(Loggers loggers, LocalServer minecraftServer, MinecraftService minecraftService, Server localServer, RestartConfiguration config, EventBus eventBus, OnlinePlayers onlinePlayers, NamedThreadFactory threads) {
this.localServer = localServer;
this.onlinePlayers = onlinePlayers;
this.logger = loggers.get(getClass());
this.minecraftServer = minecraftServer;
this.minecraftService = minecraftService;
this.config = config;
this.eventBus = eventBus;
this.threads = threads;
this.startTime = Instant.now();
}
@Override
public java.time.Duration tickPeriod() {
return config.interval();
}
@Override
public void enable() {
final Duration uptimeLimit = config.uptimeLimit();
if(uptimeLimit != null) {
// Use a timer to schedule the uptime limit restart,
// because the check in the tick method does not run
// while the server is suspended.
logger.info("Scheduling restart in " + uptimeLimit);
timer = Executors.newSingleThreadScheduledExecutor(threads.newThreadFactory("Restart timer"));
timer.schedule(
() -> requestUptimeRestart(uptimeLimit),
uptimeLimit.toMillis(),
TimeUnit.MILLISECONDS
);
}
}
@Override
public void disable() {
if(timer != null) {
timer.shutdownNow();
}
}
@Override
public void tick() {
if(!this.restartIfRequested()) {
Duration uptime = Duration.between(this.startTime, Instant.now());
Duration uptimeLimit = config.uptimeLimit();
if(uptimeLimit != null && Comparables.greaterOrEqual(uptime, uptimeLimit)) {
requestUptimeRestart(uptime);
} else {
long memory = Runtime.getRuntime().totalMemory();
long memoryLimit = config.memoryLimit();
if(memoryLimit > 0 && memory > memoryLimit) {
this.requestRestart("Exceeded memory limit (" + memory + " > " + memoryLimit + ")");
}
}
}
}
public @Nullable Instant restartRequestedAt() {
return localServer.restart_queued_at();
}
public boolean isRestartRequested() {
return localServer.restart_queued_at() != null;
}
public boolean isRestartRequested(int priority) {
return isRestartRequested() &&
localServer.restart_priority() >= priority;
}
private Set<RequestRestartEvent.Deferral> deferrals() {
return currentRequest != null ? currentRequest.deferrals()
: Collections.emptySet();
}
private boolean isDeferralTimedOut() {
return isRestartRequested() &&
config.deferTimeout() != null &&
!restartRequestedAt().plus(config.deferTimeout()).isAfter(Instant.now());
}
public boolean isRestartDeferred() {
return !(deferrals().isEmpty() || isDeferralTimedOut());
}
public ListenableFuture<?> requestUptimeRestart(Duration uptime) {
return requestRestart("Exceeded uptime limit (" + uptime + " >= " + config.uptimeLimit() + ")");
}
public ListenableFuture<?> requestRestart(String reason) {
return requestRestart(reason, ServerDoc.Restart.Priority.NORMAL);
}
public ListenableFuture<?> requestRestart(String reason, int priority) {
if(this.isRestartRequested(priority)) {
return Futures.immediateCancelledFuture();
} else {
final Instant now = Instant.now();
logger.info("Requesting restart at " + now + ", because " + reason);
return minecraftService.updateLocalServer(new ServerDoc.Restart() {
@Override public Instant restart_queued_at() { return now; }
@Override public String restart_reason() { return reason; }
@Override public int restart_priority() { return priority; }
});
}
}
public ListenableFuture<?> cancelRestart() {
if(this.isRestartRequested()) {
return minecraftService.updateLocalServer(new ServerDoc.Restart() {
@Override public Instant restart_queued_at() { return null; }
@Override public String restart_reason() { return null; }
});
} else {
return Futures.immediateCancelledFuture();
}
}
@Subscribe
public void onReconfigure(LocalServerReconfigureEvent event) {
final Instant oldTime, newTime;
final String oldReason, newReason;
final int oldPriority, newPriority;
if(event.getOldConfig() == null) {
oldTime = null;
oldReason = null;
oldPriority = ServerDoc.Restart.Priority.NORMAL;
} else {
oldTime = event.getOldConfig().restart_queued_at();
oldReason = event.getOldConfig().restart_reason();
oldPriority = event.getOldConfig().restart_priority();
}
newTime = event.getNewConfig().restart_queued_at();
newReason = event.getNewConfig().restart_reason();
newPriority = event.getNewConfig().restart_priority();
if(Objects.equals(oldTime, newTime) &&
Objects.equals(oldReason, newReason) &&
Objects.equals(oldPriority, newPriority)) return;
if(oldTime != null) {
logger.info("Restart cancelled");
currentRequest = null;
eventBus.post(new CancelRestartEvent());
}
if(newTime != null) {
logger.info("Restart requested at " + newTime +
", with " + newPriority +
" priority, because \"" + newReason + '"');
currentRequest = new RequestRestartEvent(logger, newReason, newPriority, this::restartIfRequested);
eventBus.post(currentRequest);
restartIfRequested();
}
}
private boolean shouldRestartNow() {
// If no restart requested, don't restart
if(!isRestartRequested()) return false;
// If there are deferrals, and they are not timed out, don't restart
if(isRestartDeferred()) return false;
// If there are more players online than we are allowed to kick, don't restart
if(onlinePlayers.count() > config.kickLimit()) {
logger.info("Deferring restart because more than " + config.kickLimit() + " players are online");
return false;
}
return true;
}
private boolean restartIfRequested() {
if(shouldRestartNow() && !minecraftServer.isStopping()) {
logger.info("Restarting due to request at " + restartRequestedAt());
minecraftServer.stop();
return true;
} else {
return false;
}
}
}