Kenne das Zusammenspiel zwischen Connection-Pool und JDBC-Driver

Kenne das Zusammenspiel zwischen Connection-Pool und JDBC-Driver

Framworks und Bibliotheken

Die zur Verfügung stehenden Frameworks und Bibliotheken sind eine große Erleichterung und nehmen einem viel ab, solange keine Probleme auftreten. Allerdings ist es weiterhin notwendig, ein tieferes Verständnis der Abläufe und Zusammenhänge zu haben. Bei einem aufgetretenen Problem war es außerdem gut, einen Einblick in den Source-Code der verwendeten Bibliotheken zu haben, um die Ursache von Fehlern zu verstehen und zu beheben.

Beschreibung des Fehlers

Der Fehler bestand darin, dass Daten in der Datenbank gespeichert wurden, die zu einer Transaktion gehörten, die eigentlich zurückgerollt werden sollte. Im Log hatten wir eine 'SQLTimeoutException' gesehen, die zu einem Fehler führte und auf Grund dessen eigentlich die Transaktion zurückgerollt werden sollte. Wir konnten den Fehler reproduzieren, indem wir eine Tabelle in einer separaten Transaktion gelockt haben.

Analyse des Fehlers

Jetzt, wo wir den Fehler nachstellen konnten, war es möglich, das Ganze auch zu debuggen. Relativ schnell befanden wir uns beim Debuggen dann in Code von HikariCP. Die Methode checkException der Klasse com.zaxxer.hikari.pool.ProxyConnection wird aufgerufen, wenn bei einem SQL-Statement eine Exception auftritt.

SQLExceptionOverride exceptionOverride; final SQLException checkException(SQLException sqle) { var evict = false; SQLException nse = sqle; final var exceptionOverride = poolEntry.getPoolBase().exceptionOverride; for (int depth = 0; delegate != ClosedConnection.CLOSED_CONNECTION && nse != null && depth < 10; depth++) { final var sqlState = nse.getSQLState(); if (sqlState != null && sqlState.startsWith("08") || nse instanceof SQLTimeoutException || ERROR_STATES.contains(sqlState) || ERROR_CODES.contains(nse.getErrorCode())) { if (exceptionOverride != null && exceptionOverride.adjudicate(nse) == DO_NOT_EVICT) { break; } // broken connection evict = true; break; } else { nse = nse.getNextException(); } } if (evict) { var exception = (nse != null) ? nse : sqle; LOGGER.warn("{} - Connection {} marked as broken because of SQLSTATE({}), ErrorCode({})", poolEntry.getPoolName(), delegate, exception.getSQLState(), exception.getErrorCode(), exception); leakTask.cancel(); poolEntry.evict("(connection is broken)"); delegate = ClosedConnection.CLOSED_CONNECTION; } return sqle; }

HikariCP entscheidet sich, eine Connection aus dem Pool zu entfernen, wenn eine bestimmte Fehlersituation aufgetreten ist. Dabei werden u.a. die Fehlercodes der unterschiedlichen Datenbanken bewertet. Beim Auftreten einer SQLTimeoutException wird die Connection ebenfalls aus dem Pool entfernt.

ERROR_STATES = new HashSet<>(); ERROR_STATES.add("0A000"); // FEATURE UNSUPPORTED ERROR_STATES.add("57P01"); // ADMIN SHUTDOWN ERROR_STATES.add("57P02"); // CRASH SHUTDOWN ERROR_STATES.add("57P03"); // CANNOT CONNECT NOW ERROR_STATES.add("01002"); // SQL92 disconnect error ERROR_STATES.add("JZ0C0"); // Sybase disconnect error ERROR_STATES.add("JZ0C1"); // Sybase disconnect error ERROR_CODES = new HashSet<>(); ERROR_CODES.add(500150); ERROR_CODES.add(2399);

Die Frage, die sich nun stellt: Was passiert beim Entfernen einer Connection aus dem Pool? Die Methode evict schließt die Connection.

void evict(final String closureReason) { hikariPool.closeConnection(this, closureReason); }

Bei näherer Betrachtung der Methode closeConnection fällt auf, dass dort die Methode close der eigentlichen JDBC-Connection aufgerufen wird. Was macht den eigentlich die close-Methode der JDBC-Connection? Hier die Javadoc aus java.sql.Connection

/** * Releases this {@code Connection} object's database and JDBC resources * immediately instead of waiting for them to be automatically released. * <P> * Calling the method {@code close} on a {@code Connection} * object that is already closed is a no-op. * <P> * It is <b>strongly recommended</b> that an application explicitly * commits or rolls back an active transaction prior to calling the * {@code close} method. If the {@code close} method is called * and there is an active transaction, the results are implementation-defined. * * @throws SQLException if a database access error occurs */

Der letzte Satz der Javadoc ist hier sehr interessant: "If the {@code close} method is called and there is an active transaction, the results are implementation-defined." Das heißt, es ist vom JDBC-Treiber abhängig, was in dem Fall passiert, bei dem die Connection geschlossen wird bevor ein Commit oder Rollback ausgeführt wurde. In unserem Fall kommt Oracle als Datenbank zum Einsatz. Der Oracle-Jdbc-Treiber führt in diesem Fall einen Commit aus. MySQL verhält sich anders. Wenn eine Exception vorliegt, wird ein Rollback durchgeführt. Da wir zu diesem Zeitpunkt noch kein Rollback oder Commit ausgeführt haben, wird ein Commit durchgeführt, obwohl ein Fehler vorlag. Alle bis zu diesem Zeitpunkt in der Transaktion ausgeführten SQL-Statements werden committed. Das erklärt nun unseren Fehler: Wenn unter bestimmen Fehlersituationen die Connection aus dem Pool entfernt wird erfolgt immer noch ein Commit. Ein späterer Rollback aus der Anwendung hat keine Auswirkungen mehr, bzw. führt zu einem Fehler.

Die Lösung

Ist es möglich, auf dieses Verhalten Einfluss zu nehmen? Wenn man sich den Code von HikariCP ansieht, stellt man fest, dass die Connection nur aus dem Pool entfernt wird, wenn die Methode adjudicate nicht DO_NOT_EVICT liefert. Die Variable exceptionOverride ist vom Typ SQLExceptionOverride dessen Default-Implementierung CONTINUE_EVICT liefert. \

Wir können eine eigene Implementierung dieses Interfaces erstellen und bei HikariCP registrieren, damit die Connection für bestimmte Exception nicht mehr aus dem Pool entfernt wird. Die neu erstellte Klasse muss HikariCP bekannt gemacht werden. Dies erfolgt an der HikariDataSource über die Methode setExceptionOverrideClassName.

Fazit

Für viele Fälle sind die von einer Library bereitgestellten Default-Einstellungen sinnvoll, jedoch gibt es Edge Cases, bei denen es gut ist, zu wissen, wie die einzelnen Teile zusammenspielen und sich verhalten.

← Zurück