brettwooldridge / HikariCP

光 HikariCP・A solid, high-performance, JDBC connection pool at last.
Apache License 2.0
19.63k stars 2.9k forks source link

Handle Read-Only Mode After Cluster Failover #2185

Open rreich opened 4 months ago

rreich commented 4 months ago

A AWS Aurora MySQL Cluster Failover might leave connections open to the old writer instance which is now a reader instance. (This scenario also applies to other databases and cluster solutions.)

The problem is that these connections are not closed / evicted from the pool. While it is possible to configure a connectionTestQuery like select 1 from xxx where 1=2 for update as suggested in https://github.com/brettwooldridge/HikariCP/issues/1802#issuecomment-876064227, this is not enough. A heavily used connection might not even get checked for quite some time without setting the system property com.zaxxer.hikari.aliveBypassWindowMs to 0.

Anyway, these workarounds are not optimal as they create some overhead and might have an impact on performance.

It would be much better if there was a way to handle the Exceptions when they occur and close and evict the connection right away.

For the AWS Aurora MySQL Cluster the exceptions look like this: class: java.sql.SQLException message: "The MySQL server is running with the --read-only option so it cannot execute this statement" errorCode: 1290 SQLState: "HY000"

See https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_option_prevents_statement

Some possible approaches might be:

There already are some related issues, but I found them to be lacking some detail: https://github.com/brettwooldridge/HikariCP/issues/1971 https://github.com/brettwooldridge/HikariCP/issues/1802

john9x commented 3 months ago

+1 I need weird crutches to achieve this now

class MysqlReadOnlyInterceptor implements ExceptionInterceptor {
    @Override
    public Exception interceptException(Exception ex) {
        if (ex instanceof SQLException sqlEx) {
            if (sqlEx.getErrorCode() == MysqlErrorNumbers.ER_OPTION_PREVENTS_STATEMENT
                    && sqlEx.getMessage().contains("read-only")) {

                dataSources.stream()
                        .map(HikariDataSource::getHikariPoolMXBean)
                        .filter(Objects::nonNull)
                        .forEach(pool -> {
                            log.warn("Evict connections due to read only {}", pool);
                            pool.softEvictConnections();
                        });
            }
        }
        return ex;
    }
}