mancala/apps/backend/src/main/java/app/playmancala/Backend.java

187 lines
6.2 KiB
Java
Raw Normal View History

package app.playmancala;
import java.sql.SQLException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.sql.DataSource;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import app.playmancala.auth.TokenVerifier;
import app.playmancala.game.GameClockScheduler;
import app.playmancala.game.GameManager;
import app.playmancala.game.GameRecorder;
import app.playmancala.game.GameStore;
import app.playmancala.game.LiveGames;
import app.playmancala.game.MatchMaker;
import app.playmancala.history.GameHistoryService;
import app.playmancala.history.Migrations;
import app.playmancala.history.PostgresGameHistoryRepository;
import app.playmancala.live.ValkeyCache;
import app.playmancala.live.ValkeyLiveGameStore;
import app.playmancala.loop.GameLoop;
import app.playmancala.rtmt.RtmtServer;
import io.quarkus.redis.datasource.RedisDataSource;
import io.quarkus.runtime.Startup;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
// Builds the backend from the infrastructure Quarkus provides and starts it (docs/specs/quarkus-backend, Startup).
@Startup
@ApplicationScoped
public class Backend {
private static final Logger LOG = Logger.getLogger(Backend.class);
private static final long MIGRATION_RETRY_MS = 30_000;
private static final long VALKEY_STARTUP_WAIT_MS = 5_000;
@Inject
DataSource dataSource;
@Inject
RedisDataSource redis;
@Inject
TokenVerifier tokenVerifier;
@ConfigProperty(name = "mancala.auth.legacy-userkey-auth")
String legacyUserKeyAuth;
@ConfigProperty(name = "mancala.game-clock-ms")
long gameClockMs;
private GameLoop loop;
// Valkey writes, in order.
private ExecutorService storeWriter;
// Migrations and saving finished games.
private ScheduledExecutorService recorderIo;
// Token checks.
private ExecutorService verifier;
private RtmtServer rtmt;
private GameStore gameStore;
private GameManager gameManager;
private LiveGames liveGames;
private GameHistoryService history;
private GameClockScheduler clockScheduler;
@PostConstruct
void start() {
loop = new GameLoop();
storeWriter = Executors.newSingleThreadExecutor(Thread.ofPlatform().name("valkey-writer").daemon().factory());
recorderIo = Executors.newSingleThreadScheduledExecutor(Thread.ofPlatform().name("game-recorder").daemon().factory());
verifier = Executors.newVirtualThreadPerTaskExecutor();
ValkeyLiveGameStore liveGameStore = new ValkeyLiveGameStore(redis);
PostgresGameHistoryRepository repository = new PostgresGameHistoryRepository(dataSource);
GameRecorder recorder = new GameRecorder(repository, liveGameStore, recorderIo);
history = new GameHistoryService(repository, new ValkeyCache(redis), recorder::getUnsaved);
recorder.onSaved(history::onGameSaved);
// Exactly "true", like the Node backend.
rtmt = new RtmtServer(tokenVerifier, "true".equals(legacyUserKeyAuth), loop, verifier,
RtmtServer.DEFAULT_AUTH_TIMEOUT_MS);
gameStore = new GameStore();
gameManager = new GameManager(gameStore, rtmt, new MatchMaker(rtmt), liveGameStore, recorder, storeWriter,
loop::now, gameClockMs);
liveGames = new LiveGames(rtmt, gameStore, gameManager, loop);
clockScheduler = new GameClockScheduler(gameManager::endGamesOutOfTime, loop,
GameClockScheduler.CLOCK_CHECK_INTERVAL_MS);
// The game runs without the database; saved games wait in the recorder until migrations have run.
recorderIo.execute(this::migrate);
recorderIo.scheduleWithFixedDelay(() -> guarded(recorder::retryPending), 0, GameRecorder.RETRY_INTERVAL_MS,
TimeUnit.MILLISECONDS);
// The first task on the loop, so socket events queue behind it and reconnecting players find their games.
loop.execute(() -> {
waitForValkey();
gameManager.restoreLiveGames();
clockScheduler.start();
rtmt.start();
});
}
@PreDestroy
void stop() {
loop.close();
storeWriter.shutdown();
recorderIo.shutdownNow();
verifier.shutdownNow();
}
private void migrate() {
try {
Migrations.run(dataSource);
} catch (SQLException | RuntimeException e) {
LOG.errorf("(DB) Migrations failed, retrying in %ds: %s", MIGRATION_RETRY_MS / 1000, e.getMessage());
recorderIo.schedule(this::migrate, MIGRATION_RETRY_MS, TimeUnit.MILLISECONDS);
}
}
private void waitForValkey() {
long deadline = System.currentTimeMillis() + VALKEY_STARTUP_WAIT_MS;
while (true) {
try {
redis.execute("PING");
return;
} catch (RuntimeException e) {
if (System.currentTimeMillis() >= deadline) {
LOG.error("(Valkey) Not reachable yet; starting without restoring live games");
return;
}
}
try {
Thread.sleep(250);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
private static void guarded(Runnable task) {
try {
task.run();
} catch (RuntimeException e) {
LOG.error("(Backend) Task failed", e);
}
}
public GameLoop loop() {
return loop;
}
public RtmtServer rtmt() {
return rtmt;
}
public GameStore gameStore() {
return gameStore;
}
public GameManager gameManager() {
return gameManager;
}
public LiveGames liveGames() {
return liveGames;
}
public GameHistoryService history() {
return history;
}
public TokenVerifier tokenVerifier() {
return tokenVerifier;
}
public boolean legacyUserKeyAuth() {
return "true".equals(legacyUserKeyAuth);
}
}