The Quarkus project moves to apps/backend and the Express sources go. The image is built from the repo root as before, with Temurin 25, and runs as a non-root user on port 6000. Compose passes the database as a JDBC URL with the same MANCALA_DB_PASSWORD.
89 lines
2.9 KiB
Java
89 lines
2.9 KiB
Java
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);
|
|
}
|
|
};
|
|
}
|
|
}
|