Fix MySQL/MariaDB round-stats SQL (GREATEST vs MAX) and throttle DB failure logging
Build / build (push) Successful in 1m14s

This commit is contained in:
Michael Burgess
2026-08-07 09:37:34 -04:00
parent 3a87afd3c8
commit d1d61c5d68
2 changed files with 38 additions and 4 deletions
@@ -10,7 +10,10 @@ 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.
@@ -18,17 +21,39 @@ import java.util.logging.Level;
* 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()) {
@@ -119,7 +144,7 @@ public class StatsDatabase {
}
}
} catch (SQLException ex) {
plugin.getLogger().log(Level.WARNING, "Failed to read stats for " + uuid, ex);
logFailure("read stats", uuid, ex);
}
return PlayerStats.empty(uuid);
}
@@ -142,14 +167,17 @@ public class StatsDatabase {
}
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 = MAX(best_round, ?), total_rounds_survived = total_rounds_survived + 1 WHERE uuid=?")) {
"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) {
plugin.getLogger().log(Level.WARNING, "Failed to update round stats for " + uuid, ex);
logFailure("update round stats", uuid, ex);
}
}
@@ -165,7 +193,7 @@ public class StatsDatabase {
ps.executeUpdate();
}
} catch (SQLException ex) {
plugin.getLogger().log(Level.WARNING, "Failed to update stats for " + uuid, ex);
logFailure("update stats (" + setClause + ")", uuid, ex);
}
}
}