大量データを扱うとSQLRecoverableExceptionに苛まれる

数万単位のデータをデータベースから参照、更新を行うと、まれに `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リソース関連クラス。

```java:DatabaseResourceManager.java
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を発行する簡易リポジトリクラスを用意します。

```java:Repository.java
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の発行、操作を司るメインクラスを用意。

```java:MainHandler.java
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` をやめてクローズするタイミングを手動にします。つまり、

```java:Repository.java
public static ResultSet selectFromTable() throws SQLException {
    try (
        final Connection connection = DatabaseResourceManager.openConnection();
            final Statement statement = DatabaseResourceManager.createStatement(connection)
    ) {
        return statement.executeQuery("select * from table");
    }
}
```

を、


```java:Repository.java
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発行する部分のサンプルが少ないのが辛いですね...

# 参考

- [java.sql クラス SQLRecoverableException](https://docs.oracle.com/javase/jp/6/api/java/sql/SQLRecoverableException.html)
- [JavaのJDBC接続でjava.sql.SQLRecoverableException: Closed Connectionが発生した時の解決メモ](http://rainbow-engine.com/error-sqlexception-closedconn/)
- [SqlSession の利用 - MyBatis.org](https://mybatis.org/spring/ja/sqlsession.html)