数万単位のデータをデータベースから参照、更新を行うと、まれに SQLRecoverableException
に遭遇します。
こいつほんまなんやねん...って感じて、再現の仕方もさっぱりだったのですが、つい先日再現もできたので調査内容をシェアしてみます。
実行環境
項目 | バージョン |
---|---|
Java | amazon Corretto-11.0.3.7.1 |
Spring Batch | 4.2.2.RELEASE |
データベース | Oracle Database(ドライバはojdbc8) |
mybatis | 3.3.0 |
mybatis-spring | 1.2.3 |
アプリは、定期的にデータベースを参照・更新するバッチ処理です。
サーバ上でマルチスレッドで動作し、1スレッド上で数千件ずつ処理が走ってます。
状況
Javaから、ORMに関わらず以下の例外に遭遇します。
Caused by: java.sql.SQLRecoverableException: クローズされた接続です。
at oracle.jdbc.driver.PhysicalConnection.getAutoCommit(PhysicalConnection.java:1943)
at oracle.jdbc.driver.PhysicalConnection.rollback(PhysicalConnection.java:2068)
at org.apache.commons.dbcp2.DelegatingConnection.rollback(DelegatingConnection.java:492)
at org.apache.commons.dbcp2.DelegatingConnection.rollback(DelegatingConnection.java:492)
at org.springframework.jdbc.datasource.DataSourceTransactionManager.doRollback(DataSourceTransactionManager.java:328)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:835)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.rollback(AbstractPlatformTransactionManager.java:809)
at org.springframework.transaction.support.TransactionTemplate.rollbackOnException(TransactionTemplate.java:168)
at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:144)
at org.springframework.batch.core.step.tasklet.TaskletStep$2.doInChunkContext(TaskletStep.java:273)
at org.springframework.batch.core.scope.context.StepContextRepeatCallback.doInIteration(StepContextRepeatCallback.java:82)
at org.springframework.batch.repeat.support.RepeatTemplate.getNextResult(RepeatTemplate.java:375)
at org.springframework.batch.repeat.support.RepeatTemplate.executeInternal(RepeatTemplate.java:215)
at org.springframework.batch.repeat.support.RepeatTemplate.iterate(RepeatTemplate.java:145)
at org.springframework.batch.core.step.tasklet.TaskletStep.doExecute(TaskletStep.java:258)
at org.springframework.batch.core.step.AbstractStep.execute(AbstractStep.java:208)
at org.springframework.batch.core.job.SimpleStepHandler.handleStep(SimpleStepHandler.java:148)
at org.springframework.batch.core.job.AbstractJob.handleStep(AbstractJob.java:410)
at org.springframework.batch.core.job.SimpleJob.doExecute(SimpleJob.java:136)
at org.springframework.batch.core.job.AbstractJob.execute(AbstractJob.java:319)
at org.springframework.batch.core.launch.support.SimpleJobLauncher$1.run(SimpleJobLauncher.java:147)
at org.springframework.core.task.SyncTaskExecutor.execute(SyncTaskExecutor.java:50)
at org.springframework.batch.core.launch.support.SimpleJobLauncher.run(SimpleJobLauncher.java:140)
at org.springframework.batch.core.launch.support.CommandLineJobRunner.start(CommandLineJobRunner.java:376)
at org.springframework.batch.core.launch.support.CommandLineJobRunner.main(CommandLineJobRunner.java:609)
突如接続がなくなり、データの操作に支障をきたしてました。
このアプリは、JDBCを自分で管理するのではなく、Mybatisから操作するようにしています。
ドキュメントによると SQLRecoverableException
とは
アプリケーションが回復手順を実行してトランザクション全体 (分散トランザクションの場合はトランザクションブランチ) を再試行すれば前回失敗した操作が成功する可能性があるときにスローされる SQLException のサブクラスです。
とのこと。曖昧な...
再現もよくわからなくて、 SQLRecoverableException
をキャッチしてリトライか...?と考えてましたが...
ある日突然再現できた
ドキュメントを読んでも曖昧だし、単体テストでは全然出てこないしですごい困ってたのですが...
全然関係ないプログラムを作ってたら、解決はできてないものの再現できました。
再現ソース
3種類のクラスを用意します。以下、超プライベートなサンプルなのでプログラムの厳密性は考慮してないです。
- DBリソースを操作するクラス
- リポジトリ
- 呼び出しを行うメインクラス
まずは、コネクションやステートメントを提供するDBリソース関連クラス。
public final class DatabaseResourceManager {
private DatabaseResourceManager() { }
private static final String uri = "jdbc:oracle:thin:@localhost:1521/XEPDB1";
private static final String user = "user";
private static final String password = "password";
// コネクションを生成する
public static Connection openConnection() throws SQLException {
DriverManager.registerDriver(new OracleDriver()); // <== ドライバの指定、なんか間違ってるような...
return DriverManager.getConnection(uri, user, password);
}
// ステートメントを生成する
public static Statement createStatement(Connection connection) throws SQLException {
if (connection.isClosed()) {
throw new RuntimeException("閉じられたコネクションです");
}
return connection.createStatement();
}
// ResultSetをクローズする
public static void closeResultSetConnection(ResultSet resultSet) throws SQLException {
if (resultSet.isClosed()) {
throw new SQLException("閉じられた結果セットです");
}
resultSet.close();
}
次に、コネクションやステートメントを受け取ってSQLを発行する簡易リポジトリクラスを用意します。
public final class Repository {
private Repository() { }
// にシンプルなSELECT分を発行したい
public static ResultSet selectFromTable() throws SQLException {
try (
final Connection connection = DatabaseResourceManager.openConnection();
final Statement statement = DatabaseResourceManager.createStatement(connection)
) {
return statement.executeQuery("select * from table");
}
}
// ResultSetの中身を表示するだけ
public static void printTableRow(ResultSet resultSet) throws SQLException {
try {
while (resultSet.next()) {
System.out.println(new ObjectMapper().writeValueAsString(resultSet.getCursorName()));
}
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
最後に、リソースを取得してSQLの発行、操作を司るメインクラスを用意。
public class MainHandler {
public static void main(String[] args) {
try {
final ResultSet set = Repository.selectFromExclusionMail();
Repository.printTableRow(set);
DatabaseResourceManager.closeResultSetConnection(set);
} catch (SQLException throwables) {
DatabaseResourceManager.handleSQLException(throwables);
}
}
}
で実行していく。すると...
java.sql.SQLRecoverableException: クローズされた接続です。: next
at oracle.jdbc.driver.InsensitiveScrollableResultSet.ensureOpen(InsensitiveScrollableResultSet.java:108)
at oracle.jdbc.driver.InsensitiveScrollableResultSet.next(InsensitiveScrollableResultSet.java:402)
at Repository.printTableRow(Repository.java:40)
at MainHandler.main(MainHandler.java:36)
8:53:19: Task execution finished 'MainHandler.main()'.
おおおお...本番でたまに出てきてわけわからんあいつでたーーーー
たしかに、whileでカーソルをくるくる回す部分で接続がクローズされている。
原因
再現方法がわかっただけで、原因はわからないです...
が、結果から言えることとしてResultSetを取得して操作する前にリソースをcloseしてはならないらしい。
解決?
サンプルの、生JDBCではあっさり解決できました。リポジトリクラスのSQLを発行する部分で、 try-with-resource
をやめてクローズするタイミングを手動にします。つまり、
public static ResultSet selectFromTable() throws SQLException {
try (
final Connection connection = DatabaseResourceManager.openConnection();
final Statement statement = DatabaseResourceManager.createStatement(connection)
) {
return statement.executeQuery("select * from table");
}
}
を、
public static ResultSet selectFromTable() throws SQLException {
// ステートメントを生成
final Statement statement = DatabaseResourceManager.createStatement(DatabaseResourceManager.openConnection())
// SQLを発行
return statement.executeQuery("select * from table");
}
に。これでとりあえずサンプルは正常終了できました。
本題のバッチは、Mybatisが提供している SqlSession
というセッションを管理するクラスのインスタンスをSQL実行前に取得し、実行後にクローズするように修正してみました。
これでしばらく様子を見てみようかと。
Mybatisのドキュメント、 SqlSessionFactory
からセッション開いてSQL発行する部分のサンプルが少ないのが辛いですね...