Files
BlockParty/src/main/java/us/tss3/blockparty/persistence/StatsDatabase.java
T

200 lines
7.9 KiB
Java

package us.tss3.blockparty.persistence;
import org.bukkit.plugin.Plugin;
import us.tss3.blockparty.config.ConfigManager.StorageSettings;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
/** SQLite- or MySQL-backed statistics storage, selected via storage.type in config.yml.
* All public methods are safe to call from any thread; callers are expected to invoke
* them off the main thread (see StatsManager). */
public class StatsDatabase {
private static final long ERROR_LOG_INTERVAL_MS = TimeUnit.MINUTES.toMillis(5);
private final Plugin plugin;
private final StorageSettings settings;
private final String table;
private Connection connection;
/** operation name -> last time we actually logged a failure for it, so a persistently
* broken database (bad SQL, dropped connection, etc.) logs once immediately for
* diagnosis and then at most once every {@link #ERROR_LOG_INTERVAL_MS} after that,
* instead of spamming a full stack trace on every round/query. */
private final Map<String, Long> lastLoggedAt = new ConcurrentHashMap<>();
public StatsDatabase(Plugin plugin, StorageSettings settings) {
this.plugin = plugin;
this.settings = settings;
this.table = settings.isMysql() ? settings.tablePrefix() + "player_stats" : "player_stats";
}
private void logFailure(String operation, UUID uuid, SQLException ex) {
long now = System.currentTimeMillis();
Long last = lastLoggedAt.get(operation);
if (last == null) {
plugin.getLogger().log(Level.WARNING, "Failed to " + operation + " for " + uuid
+ " (further repeats of this error are throttled to once per 5 minutes)", ex);
lastLoggedAt.put(operation, now);
} else if (now - last >= ERROR_LOG_INTERVAL_MS) {
plugin.getLogger().warning("Still failing to " + operation + ": "
+ ex.getClass().getSimpleName() + ": " + ex.getMessage());
lastLoggedAt.put(operation, now);
}
}
public synchronized void connect() {
try {
if (settings.isMysql()) {
connectMysql();
} else {
connectSqlite();
}
try (Statement st = connection.createStatement()) {
st.execute(createTableStatement());
}
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to initialize " +
(settings.isMysql() ? "MySQL" : "SQLite") + " stats database", ex);
}
}
private void connectSqlite() throws Exception {
File dbFile = new File(plugin.getDataFolder(), "blockparty.db");
Class.forName("org.sqlite.JDBC");
connection = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath());
}
private void connectMysql() throws Exception {
Class.forName("com.mysql.cj.jdbc.Driver");
String extra = settings.connectionParameters() == null ? "" : settings.connectionParameters().trim();
StringBuilder url = new StringBuilder("jdbc:mysql://")
.append(settings.host()).append(':').append(settings.port()).append('/').append(settings.database())
.append("?autoReconnect=true&characterEncoding=utf8");
if (!extra.isEmpty()) {
url.append('&').append(extra);
}
connection = DriverManager.getConnection(url.toString(), settings.username(), settings.password());
}
private String createTableStatement() {
if (settings.isMysql()) {
return "CREATE TABLE IF NOT EXISTS " + table + " (" +
"uuid VARCHAR(36) PRIMARY KEY," +
"games_played INT NOT NULL DEFAULT 0," +
"wins INT NOT NULL DEFAULT 0," +
"eliminations INT NOT NULL DEFAULT 0," +
"best_round INT NOT NULL DEFAULT 0," +
"total_rounds_survived INT NOT NULL DEFAULT 0" +
")";
}
return "CREATE TABLE IF NOT EXISTS " + table + " (" +
"uuid TEXT PRIMARY KEY," +
"games_played INTEGER NOT NULL DEFAULT 0," +
"wins INTEGER NOT NULL DEFAULT 0," +
"eliminations INTEGER NOT NULL DEFAULT 0," +
"best_round INTEGER NOT NULL DEFAULT 0," +
"total_rounds_survived INTEGER NOT NULL DEFAULT 0" +
")";
}
public synchronized void close() {
try {
if (connection != null && !connection.isClosed()) {
connection.close();
}
} catch (SQLException ignored) {
}
}
private synchronized void ensureRow(String uuid) throws SQLException {
String sql = settings.isMysql()
? "INSERT IGNORE INTO " + table + "(uuid) VALUES (?)"
: "INSERT OR IGNORE INTO " + table + "(uuid) VALUES (?)";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setString(1, uuid);
ps.executeUpdate();
}
}
public synchronized PlayerStats getStats(UUID uuid) {
if (connection == null) {
return PlayerStats.empty(uuid);
}
try {
ensureRow(uuid.toString());
try (PreparedStatement ps = connection.prepareStatement(
"SELECT games_played, wins, eliminations, best_round, total_rounds_survived FROM " + table + " WHERE uuid=?")) {
ps.setString(1, uuid.toString());
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return new PlayerStats(uuid, rs.getInt(1), rs.getInt(2), rs.getInt(3), rs.getInt(4), rs.getInt(5));
}
}
}
} catch (SQLException ex) {
logFailure("read stats", uuid, ex);
}
return PlayerStats.empty(uuid);
}
public synchronized void incrementGamesPlayed(UUID uuid) {
update(uuid, "games_played = games_played + 1");
}
public synchronized void incrementWins(UUID uuid) {
update(uuid, "wins = wins + 1");
}
public synchronized void incrementEliminations(UUID uuid) {
update(uuid, "eliminations = eliminations + 1");
}
public synchronized void recordRoundReached(UUID uuid, int round) {
if (connection == null) {
return;
}
try {
ensureRow(uuid.toString());
// MySQL/MariaDB's MAX() is aggregate-only; the two-argument "greatest of these
// values" form used by SQLite must be GREATEST() on that dialect instead.
String greatestFn = settings.isMysql() ? "GREATEST" : "MAX";
try (PreparedStatement ps = connection.prepareStatement(
"UPDATE " + table + " SET best_round = " + greatestFn + "(best_round, ?), total_rounds_survived = total_rounds_survived + 1 WHERE uuid=?")) {
ps.setInt(1, round);
ps.setString(2, uuid.toString());
ps.executeUpdate();
}
} catch (SQLException ex) {
logFailure("update round stats", uuid, ex);
}
}
private void update(UUID uuid, String setClause) {
if (connection == null) {
return;
}
try {
ensureRow(uuid.toString());
try (PreparedStatement ps = connection.prepareStatement(
"UPDATE " + table + " SET " + setClause + " WHERE uuid=?")) {
ps.setString(1, uuid.toString());
ps.executeUpdate();
}
} catch (SQLException ex) {
logFailure("update stats (" + setClause + ")", uuid, ex);
}
}
}