Skip to content

fix(db): validate connection before use to recover stale sessions - #695

Merged
Pugmatt merged 2 commits into
Pugmatt:masterfrom
PaulW:fix/mariadb-stale-connection
Sep 4, 2026
Merged

fix(db): validate connection before use to recover stale sessions#695
Pugmatt merged 2 commits into
Pugmatt:masterfrom
PaulW:fix/mariadb-stale-connection

Conversation

@PaulW

@PaulW PaulW commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Database holds a single long-lived JDBC Connection reused for every query.
When the server (or a proxy such as MaxScale) closes the idle session, the
driver does not know the connection is dead, so the next query throws and the
player is kicked before they reach the server list. This validates the
connection before use and reopens it if it is dead.

Problem

After the process has been idle for a while (~10 minutes, matching a typical
wait_timeout / proxy idle timeout), the first player to connect is kicked
and the log shows:

A database error has occured

Repeated attempts keep failing, then one eventually succeeds and the service
works until the next idle period.

Root cause

  1. Database.openConnection() opens one Connection at startup; every query
    in DataUtil reuses that same connection. There is no pool, so a stale
    session is neither detected nor replaced.
  2. BC_AUTO_RECONNECT=true appends &autoReconnect=true to the JDBC URL, but
    that option is a no-op on recent drivers:
    • MariaDB Connector/J 3.x removed autoReconnect — it is listed under
      "Removed options" (since 1.1.7, removed in 3.0.0) and silently ignored
      [1].
    • PostgreSQL JDBC has no autoReconnect connection parameter at all, so
      it is silently ignored [2].
    • MySQL Connector/J still defines autoReconnect (and
      autoReconnectForPools) in the current driver source [3];
      its own property description states "The use of this feature is not
      recommended, because it has side effects related to session state and data
      consistency" [4].
  3. The keep-alive timer in BedrockConnect.java only reopens when
    connection.isClosed() is already true — which the driver does not report
    for a server-closed session — so it cannot reliably recover from the stale
    session.

The fix

Database.getConnection() validates the connection with Connection.isValid()
— a standard JDBC 4.0 liveness check (a protocol ping) — and reopens it on
failure:

public synchronized Connection getConnection() {
    // Validate the connection is alive before use and reopen if the server
    // closed the idle session. Gated by autoReconnect so the option remains
    // the single switch for connection recovery across all drivers.
    if (autoReconnect) {
        try {
            if (connection == null || connection.isClosed() || !connection.isValid(2)) {
                BedrockConnect.logger.debug("Database connection is invalid or closed; reopening");
                return openConnection();
            }
        } catch (SQLException e) {
            BedrockConnect.logger.error("Database connection validation failed; reopening", e);
            return openConnection();
        }
    }
    return connection;
}
  • isValid(2) is the lightweight ping recommended by the MariaDB Connector/J
    docs ("Connection.isValid() is doing a ping") [1]. It is
    standard JDBC 4.0 (Since 1.6) [5], so it works for the mysql,
    mariadb and postgres branches.
  • synchronized serialises the validate-and-reopen so two query threads cannot
    race a reconnect and clobber the shared connection.
  • It is gated by autoReconnect so the existing BC_AUTO_RECONNECT option
    remains the single switch for connection recovery across all drivers:
    • true — validate + reopen (for MariaDB/PostgreSQL this is the recovery,
      since their autoReconnect URL param does nothing; for MySQL it supplements
      the driver's own autoReconnect, which its docs advise against relying on
      [4]).
    • false — return the raw connection (original behaviour), so an operator can
      deliberately surface stale-session failures rather than mask them.
  • It fixes both the per-query path (DataUtil) and the keep-alive timer, which
    also calls getConnection().

Why this approach

A driver-level autoReconnect is not a reliable solution: it is removed in
MariaDB Connector/J 3.x [1], non-existent in PostgreSQL JDBC
[2], and MySQL Connector/J's documentation advises against
relying on it [4]. The recommended pattern across all three
drivers is to validate connections before use (which a connection pool does
internally via isValid()). This change applies that same validation to the
existing single-connection model, gated by the existing BC_AUTO_RECONNECT
option, with no new dependencies.

Testing

Deployed behind a MaxScale proxy whose service wait_timeout is 540s (9 min).
After the fix, a player connecting after a 20-minute idle period connects
cleanly with no A database error has occured. Behaviour for
BC_AUTO_RECONNECT=true (the default) is unchanged recovery; false returns
the original raw-connection behaviour.

References

  1. MariaDB Connector/J documentation — "Removed options" (autoReconnect,
    since 1.1.7, removed in 3.0.0) and "How to Do a Lightweight Ping"
    (Connection.isValid() is a protocol ping):
    https://github.com/mariadb-corporation/mariadb-docs/blob/main/connectors/mariadb-connector-j/about-mariadb-connector-j.md
  2. PostgreSQL JDBC connection parameters (no autoReconnect parameter exists):
    https://jdbc.postgresql.org/documentation/use/
  3. MySQL Connector/J PropertyKeyautoReconnect and
    autoReconnectForPools are still defined in the current driver source:
    https://github.com/mysql/mysql-connector-j/blob/trunk/src/main/core-api/java/com/mysql/cj/conf/PropertyKey.java
  4. MySQL Connector/J LocalizedErrorMessages.properties — the autoReconnect
    property description: "The use of this feature is not recommended, because it
    has side effects related to session state and data consistency when
    applications don't handle SQLExceptions properly":
    https://github.com/mysql/mysql-connector-j/blob/trunk/src/main/resources/com/mysql/cj/LocalizedErrorMessages.properties
  5. JDK java.sql.Connection.isValid(int) javadoc (Oracle Java SE 8) — "The
    driver shall submit a query on the connection or use some other mechanism
    that positively verifies the connection is still valid" (Since 1.6 / JDBC
    4.0):
    https://docs.oracle.com/javase/8/docs/api/java/sql/Connection.html#isValid-int-

Database holds a single long-lived JDBC Connection reused for every query.
When the server (or a proxy such as MaxScale) closes the idle session, the
driver does not know it is dead, so the next query throws and the player is
kicked before reaching the server list ("A database error has occured").

BC_AUTO_RECONNECT's &autoReconnect=true URL param does not provide recovery:
MariaDB Connector/J removed autoReconnect in 3.0.0 [1]; PostgreSQL JDBC never
had it [2]; MySQL Connector/J still defines it [3] but its own property
description states it is "not recommended" due to session-state and
data-consistency side effects [4]. Validate the connection with
Connection.isValid() (a JDBC 4.0 protocol ping [1, 5]) before use and reopen on
failure, gated by the existing autoReconnect option so it remains the single
switch for recovery across all drivers.

[1] https://github.com/mariadb-corporation/mariadb-docs/blob/main/connectors/mariadb-connector-j/about-mariadb-connector-j.md
[2] https://jdbc.postgresql.org/documentation/use/
[3] https://github.com/mysql/mysql-connector-j/blob/trunk/src/main/core-api/java/com/mysql/cj/conf/PropertyKey.java
[4] https://github.com/mysql/mysql-connector-j/blob/trunk/src/main/resources/com/mysql/cj/LocalizedErrorMessages.properties
[5] https://docs.oracle.com/javase/8/docs/api/java/sql/Connection.html#isValid-int-
@PaulW
PaulW force-pushed the fix/mariadb-stale-connection branch from ad7cce0 to 68b4462 Compare September 1, 2026 16:19
@Pugmatt
Pugmatt merged commit 315f306 into Pugmatt:master Sep 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants