diff --git a/README.md b/README.md index f1fd2f7..c7e5769 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,12 @@ Common values: - `sslMode=VERIFY_IDENTITY` — TLS mandatory and the server certificate is validated against a trust store (production-grade; requires a properly signed/trusted cert). +MySQL and MariaDB are both supported through the same `mysql` driver/config. If a stats +query ever fails (bad credentials, dropped connection, etc.), BlockParty logs the full error +once for diagnosis and then throttles repeats of that same failure to once every 5 minutes — +it never spams a stack trace on every round, and gameplay is unaffected either way since all +stats I/O is async and best-effort. + The MySQL JDBC driver (`mysql-connector-j`) is bundled in the plugin jar, so no extra download is required. The stats table is created automatically on startup if it doesn't exist. All queries run asynchronously off the main thread regardless of storage type. diff --git a/src/main/java/us/tss3/blockparty/persistence/StatsDatabase.java b/src/main/java/us/tss3/blockparty/persistence/StatsDatabase.java index fcc7505..c0b9960 100644 --- a/src/main/java/us/tss3/blockparty/persistence/StatsDatabase.java +++ b/src/main/java/us/tss3/blockparty/persistence/StatsDatabase.java @@ -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 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); } } }