mancala/apps/backend/src/main/java/app/playmancala/loop/GameLoop.java

89 lines
2.9 KiB
Java
Raw Normal View History

package app.playmancala.loop;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.jboss.logging.Logger;
// The one thread that owns all live game state: games, matchmaking, previews and the WebSocket registry. Like the
// Node backend's event loop, it lets that state go without locks. Never block it on I/O.
public final class GameLoop implements Scheduler, AutoCloseable {
private static final Logger LOG = Logger.getLogger(GameLoop.class);
private final ScheduledExecutorService executor;
private volatile Thread thread;
public GameLoop() {
executor = Executors.newSingleThreadScheduledExecutor(runnable -> {
Thread loopThread = new Thread(runnable, "game-loop");
loopThread.setDaemon(true);
thread = loopThread;
return loopThread;
});
}
@Override
public long now() {
return System.currentTimeMillis();
}
@Override
public void execute(Runnable task) {
executor.execute(guarded(task));
}
@Override
public Cancellable schedule(Runnable task, long delayMs) {
Future<?> future = executor.schedule(guarded(task), delayMs, TimeUnit.MILLISECONDS);
return () -> future.cancel(false);
}
@Override
public Cancellable scheduleAtFixedRate(Runnable task, long periodMs) {
Future<?> future = executor.scheduleAtFixedRate(guarded(task), periodMs, periodMs, TimeUnit.MILLISECONDS);
return () -> future.cancel(false);
}
// Runs the task on the loop and waits for its result; for threads that need to read live state.
public <T> T call(Callable<T> task) {
if (Thread.currentThread() == thread) {
try {
return task.call();
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
try {
return executor.submit(task).get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while waiting for the game loop", e);
} catch (ExecutionException e) {
if (e.getCause() instanceof RuntimeException runtime) throw runtime;
throw new IllegalStateException(e.getCause());
}
}
@Override
public void close() {
executor.shutdownNow();
}
// An exception must not stop a repeating task or reach the executor, which would swallow it.
private static Runnable guarded(Runnable task) {
return () -> {
try {
task.run();
} catch (Throwable e) {
LOG.error("(GameLoop) Task failed", e);
}
};
}
}