RM-BLOG

IT系技術職のおっさんがIT技術とかライブとか日常とか雑多に語るブログです。* 本ブログに書かれている内容は個人の意見・感想であり、特定の組織に属するものではありません。/All opinions are my own.*

いくつかのDBのコネクションを強制的にぶち切る実験をしてみた

タイトルの通り。
最初はポスグレで始めたのだが、そういやこれ他のDBだとどうなるのかなと思って実験してみた。

はじめに

作業は一部除いて基本ローカルで完結させた。
今はDockerあればDBですら簡単に手元にそろうってのは素敵な時代ですなあ。
なお、DBに接続するクライアント側のちょっとしたプログラム(強制的にぶち切られる方)はホストマシン上にJavaで組む。 言語がJavaであることに拘りはなくて、なんとなくJavaを選んだだけである。

強制的にぶち切られるプログラム

以下のようなコードのプログラムを動かす。
以下はポスグレの例だが、DBのURLと使うJDBCドライバが違う以外、大体全DBで同じ。

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.PreparedStatement;

public class Connect2PostgresTest {
    public static void main(String[] args) throws Exception {
        Connection con = null;
        PreparedStatement ps = null;
        try {
            String url = "jdbc:postgresql://localhost:5432/postgres";
            String username = "postgres";
            String password = "";
            System.out.println("start connection");
            con = DriverManager.getConnection(url, username, password);
            con.setAutoCommit(false);

            System.out.println("start waiting 60 seconds");
            Thread.sleep(60000);

            System.out.println("start creating PreparedStatement");
            ps = con.prepareStatement("SELECT 1");

            System.out.println("start executing query");
            ResultSet rs = ps.executeQuery();
            while(rs.next()) {
                String col = rs.getString(1);
                System.out.println(col);
            }

            System.out.println("start waiting 60 seconds");
            Thread.sleep(60000);

            rs.close();
        } catch(Exception e) {
            e.printStackTrace();
            throw e;
        } finally {
            if (ps != null) {
                System.out.println("start colosing PreparedStatement");
                ps.close();
            }
            if (con != null) {
                System.out.println("start colosing Connection");
                con.close();
            }
        }
        System.out.println("end");
    }    
}

見れば分かるが、別に全然大したことはしていない。
Connection繋いで、60秒待ち(A)、SQL発行して、また60秒待ち(B)、終わり。
SQLも固定値を取得してくるだけの凄い単純なものである(SQLの内容は本旨ではないので適当でいい)
途中待ち時間として60秒待たせてるのは、その間に裏でセッション特定して殺す準備をするため。
殺すタイミングによって例外の有無があるようなので、(A)と(B)で待つタイミングを2つ用意してみた。

Postgresql

(1) JDBCドライバのダウンロード

ここからPostgres用のJDBCドライバをダウンロードしてくる。
自分のときは「postgresql-42.2.23.jar」というjarファイルを使った。

(2) docker run

ポスグレDBをDockerで起動。

> docker run --rm -d -p 5432:5432 -v postgres-vol:/var/lib/postgresql/data -e POSTGRES_HOST_AUTH_METHOD=trust postgres:latest
  • ホスト側のポートはコンテナ側のポートと合わせて同じ5432にしているが、ここは個々の環境に合わせて要変更。
    また、ボリュームの設定(-vオプション)も適当なので、実際の実行環境に合わせて要変更。
  • POSTGRES_HOST_AUTH_METHOD=trustはパスワードなしで入れるようにする設定で、実運用を考えるとありえないが、今回の実験用なので一旦これでいい。
    どうせ用が済んだらすぐにこの子にも死んでもらうのだ(!?)※docker stopでコンテナを停止させるだけだよ!

(3) クライアント側プログラム実行

とりあえずまずコンパイルする。
見た目上は標準ライブラリしか使ってないのでjavac Connect2PostgresTest.javaだけでコンパイル可能である

続いて実行である。
実行時にクラスパスに(1)のJDBCドライバを通す。
私の実行環境がWindowsだったので、コマンドプロンプトだと

> java -cp .;.\postgresql-42.2.23.jar Connect2PostgresTest

Powershellだと

> java -cp ".;.\postgresql-42.2.23.jar" Connect2PostgresTest

Linuxだと

> java -cp .:./postgresql-42.2.23.jar Connect2PostgresTest

クラスパスの通し方は環境で微妙に変わるので面倒くさいですよね。。。

(4) セッション切断

セッションを探す。

select * from pg_stat_activity where datname = 'postgres'

探し当てたセッションのPIDを引数に以下SQLを実行する。
以下はPIDが100だったときのケース

select pg_terminate_backend('100');

(5)結果発表おおおおお!!

(A)コネクション繋いでからSQL発行する前に切断

PreparedStatement#executeQueryで落ちる(例外が発生する)。
この場合はfinally句で定義しているConnection#closeも落ちる。
ただResultSet#closeは問題なく処理される(例外が発生しない)らしい。そういうもんか。

start connection
start waiting 60 seconds
start creating PreparedStatement
start executing query
org.postgresql.util.PSQLException: FATAL: terminating connection due to administrator command
        at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2552)
        at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2284)
        at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:322)
        at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:481)
        at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:401)
        at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:164)
        at org.postgresql.jdbc.PgPreparedStatement.executeQuery(PgPreparedStatement.java:114)
        at Connect2PostgresTest.main(Connect2PostgresTest.java:28)
start colosing PreparedStatement
start colosing Connection
Exception in thread "main" org.postgresql.util.PSQLException: FATAL: terminating connection due to administrator command        
        at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2552)
        at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2284)
        at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:322)
        at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:481)
        at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:401)
        at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:164)
        at org.postgresql.jdbc.PgPreparedStatement.executeQuery(PgPreparedStatement.java:114)
        at Connect2PostgresTest.main(Connect2PostgresTest.java:28)

(B)SQL発行してからResultSet(とかConnectionとか)閉じるまでに切断

こっちは何も問題なくプログラムが正常終了した。
Connection#closeあたりで死ぬんじゃないかと思ったが閉じれる(少なくとも例外発生はしない形で処理される)らしい。
この辺りは恐らくJDBCドライバの実装に依存しているのだろう。
少なからず直感と反する動きなので若干戸惑ったが、まあ、そういうもんなんだと思うことにして次に進む。

MySQL

(1) JDBCドライバのダウンロード

ここからインストーラーをダウンロードする。
ダウンロードしたインストーラーを実行し、「Add」→「MySQL Connectiors」→「Connector/J」を選んでNext押してってインストール。
インストール後のログを見れば、jarファイルが書き出されたパスが載ってるので、そこを見て、jarファイルをコピーしてくる。
自分の時は「mysql-connector-java-8.0.26.jar」っていうファイルだった。

(1)-2 プログラムの修正

↑で使ったポスグレ用のプログラムをちょっと改造する。
といってもURLとDriverManagerの引数部分だけであるが。。。
一応Javaファイル自体わけていて、今回のファイルは「Connect2MySQLTest.java」とした。

...(略)...
            String url = "jdbc:mysql://localhost/sys?user=root&password=mysqlroot";
            //String username = "postgres";
            //String password = "";
            System.out.println("start connection");
            //con = DriverManager.getConnection(url, username, password);
            con = DriverManager.getConnection(url);
...(略)...
  • ユーザーはrootで、パスワードは「mysqlroot」という実運用考えるとあり得ない設定だが、単にコネクションの強制切断実験だから別にいいのだ。
    なお、パスワードはDocker起動時に指定するので、実際の起動時の設定に合わせて変更する。

(2) docker run

MySQLをDockerで起動

> docker run --rm -p 3306:3306 -d -e MYSQL_ROOT_PASSWORD=mysqlroot mysql:latest
  • MYSQL_ROOT_PASSWORDでrootユーザーのパスワードを指定する。この例では「mysqlroot」で、(1)で修正したプログラムで指定した内容と同じ値である。変更するならプログラム側も合わせて変更すること。
  • ポートの3306番も環境に合わせて変えていいが、その場合プログラム側のDB URLに明示的にポートを指定してあげる必要がある。
    例えば3307番を使うならDocker runはdocker run --rm -p 3307:3306 ...(略)...にしてDB URLの値は"jdbc:mysql://localhost:3307/sys?user=root&password=mysqlroot"みたいにlocalhostの後ろにポートを記述する(ポート未指定だと3306番になる)

(3) クライアント側プログラム実行

コンパイルはポスグレ時と同様、単純にjavac Connect2MySQLTest.javaで通る。
実行についても、クラスパスに設定するJDBCドライバ(のjarファイル)が異なる以外はポスグレと同様。
コマンドプロンプトだと

> java -cp .;.\mysql-connector-java-8.0.26.jar Connect2MySQLTest

Powershellだと

> java -cp ".;.\mysql-connector-java-8.0.26.jar" Connect2MySQLTest

Linuxだと

> java -cp .:./mysql-connector-java-8.0.26.jar Connect2MySQLTest

(4) セッション切断

セッションを探す。

SELECT * FROM information_schema.PROCESSLIST 

ポスグレと違ってJDBCで接続したかどうかの識別情報がないので分かりづらいが(これは私が知らないだけかもしれない)、「TIME」項目が接続からの経過秒数となるのと、上記のSQLを投げた直後に返ってくる「INFO」項目には、上記のSQLそのものが入ってるので、そのレコードのIDは検査用のセッションであるとわかるなど、そのあたりの情報から何となく判断が付く。
(そもそもローカルで立ち上げてるDockerのプロセスだから繋いでるセッションは超少ない。探す用のセッション、event_schedulerのほかに1つくらいしかないはず)

セッションを切断する場合は、上記のSQLで返ってくる「ID」の値をKILLコマンドの引数に渡して実行するだけ。
IDが100なら以下の通り。

KILL 100;

(5)結果発表おおおおお!!

(A)コネクション繋いでからSQL発行する前に切断

PreparedStatement#executeQueryで落ちる(例外が発生する)。
その後のfinally句のConnection#closeでも落ちるので、結果が結構騒がしい(?)ことになる。
ただPreparedStatement#closeは落ちない。そういうもんなのか。

start connection
start waiting 60 seconds
start creating PreparedStatement
start executing query
com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

The last packet successfully received from the server was 60,089 milliseconds ago. The last packet sent successfully to the server was 60,091 milliseconds ago.
        at com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException(SQLError.java:174)
        at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:64)
        at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:953)
        at com.mysql.cj.jdbc.ClientPreparedStatement.executeQuery(ClientPreparedStatement.java:1003)
        at Connect2MySQLTest.main(Connect2MySQLTest.java:27)
Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure

The last packet successfully received from the server was 60,089 milliseconds ago. The last packet sent successfully to the server was 60,091 milliseconds ago.
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
        at java.lang.reflect.Constructor.newInstance(Unknown Source)
        at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:61)
        at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:105)
        at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:151)
        at com.mysql.cj.exceptions.ExceptionFactory.createCommunicationsException(ExceptionFactory.java:167)
        at com.mysql.cj.protocol.a.NativeProtocol.readMessage(NativeProtocol.java:519)
        at com.mysql.cj.protocol.a.NativeProtocol.checkErrorMessage(NativeProtocol.java:683)
        at com.mysql.cj.protocol.a.NativeProtocol.sendCommand(NativeProtocol.java:622)
        at com.mysql.cj.protocol.a.NativeProtocol.sendQueryPacket(NativeProtocol.java:970)
        at com.mysql.cj.NativeSession.execSQL(NativeSession.java:662)
        at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:930)
        ... 2 more
Caused by: java.io.EOFException: Can not read response from server. Expected to read 4 bytes, read 0 bytes before connection was unexpectedly lost.
        at com.mysql.cj.protocol.FullReadInputStream.readFully(FullReadInputStream.java:67)
        at com.mysql.cj.protocol.a.SimplePacketReader.readHeader(SimplePacketReader.java:63)
        at com.mysql.cj.protocol.a.SimplePacketReader.readHeader(SimplePacketReader.java:45)
        at com.mysql.cj.protocol.a.TimeTrackingPacketReader.readHeader(TimeTrackingPacketReader.java:52)
        at com.mysql.cj.protocol.a.TimeTrackingPacketReader.readHeader(TimeTrackingPacketReader.java:41)
        at com.mysql.cj.protocol.a.MultiPacketReader.readHeader(MultiPacketReader.java:54)
        at com.mysql.cj.protocol.a.MultiPacketReader.readHeader(MultiPacketReader.java:44)
        at com.mysql.cj.protocol.a.NativeProtocol.readMessage(NativeProtocol.java:513)
        ... 7 more
start colosing PreparedStatement
start colosing Connection
Exception in thread "main" com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

The last packet successfully received from the server was 60,089 milliseconds ago. The last packet sent successfully to the server was 60,091 milliseconds ago.
        at com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException(SQLError.java:174)
        at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:64)
        at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:953)
        at com.mysql.cj.jdbc.ClientPreparedStatement.executeQuery(ClientPreparedStatement.java:1003)
        at Connect2MySQLTest.main(Connect2MySQLTest.java:27)
Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure

The last packet successfully received from the server was 60,089 milliseconds ago. The last packet sent successfully to the server was 60,091 milliseconds ago.
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
        at java.lang.reflect.Constructor.newInstance(Unknown Source)
        at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:61)
        at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:105)
        at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:151)
        at com.mysql.cj.exceptions.ExceptionFactory.createCommunicationsException(ExceptionFactory.java:167)
        at com.mysql.cj.protocol.a.NativeProtocol.readMessage(NativeProtocol.java:519)
        at com.mysql.cj.protocol.a.NativeProtocol.checkErrorMessage(NativeProtocol.java:683)
        at com.mysql.cj.protocol.a.NativeProtocol.sendCommand(NativeProtocol.java:622)
        at com.mysql.cj.protocol.a.NativeProtocol.sendQueryPacket(NativeProtocol.java:970)
        at com.mysql.cj.NativeSession.execSQL(NativeSession.java:662)
        at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:930)
        ... 2 more
Caused by: java.io.EOFException: Can not read response from server. Expected to read 4 bytes, read 0 bytes before connection was unexpectedly lost.
        at com.mysql.cj.protocol.FullReadInputStream.readFully(FullReadInputStream.java:67)
        at com.mysql.cj.protocol.a.SimplePacketReader.readHeader(SimplePacketReader.java:63)
        at com.mysql.cj.protocol.a.SimplePacketReader.readHeader(SimplePacketReader.java:45)
        at com.mysql.cj.protocol.a.TimeTrackingPacketReader.readHeader(TimeTrackingPacketReader.java:52)
        at com.mysql.cj.protocol.a.TimeTrackingPacketReader.readHeader(TimeTrackingPacketReader.java:41)
        at com.mysql.cj.protocol.a.MultiPacketReader.readHeader(MultiPacketReader.java:54)
        at com.mysql.cj.protocol.a.MultiPacketReader.readHeader(MultiPacketReader.java:44)
        at com.mysql.cj.protocol.a.NativeProtocol.readMessage(NativeProtocol.java:513)
        ... 7 more

(B)SQL発行してからResultSet(とかConnectionとか)閉じるまでに切断

ポスグレと違い、MySQLの場合はこのタイミングでぶち切ったらConnection#closeが落ちる(例外が発生する)。
ただResultSet#closePreparedStatement#closeは正常に処理されるらしい。

start connection
start waiting 60 seconds
start creating PreparedStatement
start executing query
1
start waiting 60 seconds
start colosing PreparedStatement
start colosing Connection
Exception in thread "main" java.sql.SQLNonTransientConnectionException: Communications link failure during rollback(). Transaction resolution unknown.
        at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:110)
        at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)
        at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:89)
        at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:63)
        at com.mysql.cj.jdbc.ConnectionImpl.rollback(ConnectionImpl.java:1837)
        at com.mysql.cj.jdbc.ConnectionImpl.realClose(ConnectionImpl.java:1694)
        at com.mysql.cj.jdbc.ConnectionImpl.close(ConnectionImpl.java:713)
        at Connect2MySQLTest.main(Connect2MySQLTest.java:47)

Oracle

(0)DBの用意&ウォレットファイルの取得

Oracleは他の2つ(ポスグレとMySQL)のように、docker pullしてきてすぐ使えるというわけではなく、提供されているContainer Buildのツールを使ってイメージの作成をいちいち自前で実行しなければならないという手間がある(ここ参照)
これが非常に面倒くさいので、OracleだけはDockerは使わず、Oracle Cloudの提供しているADB(Autonomous Database)を利用する。
これはストレージなどの制限はあるが無料で使えるManagedのOracle Databaseである。
そもそも「強制的にコネクションをぶち切る」ことが目的であり、「ローカルにDBたてる」ことは本旨ではない。
Docker Image作るよりはこっちのほうがはるかに楽なので、もうこれでいいやと割り切ることにした。

Oracle Cloud上でのADBの作り方などは公式Doc私の過去の別記事などを参考にしていただきたい。
ただ今(2021年9月)見るとこのページに載っているスクショは最新版とは乖離があるようで、少し古い内容だったので、これから作ろうという方がいたら注意されたい(基本は同じである)。

DBつくったら、OCIの管理コンソール上から作ったDBを選んで詳細画面に遷移後、「DB Connection」のボタンからウォレットファイルをダウンロードし、プログラムと同階層に配置して解凍する。

(1) JDBCドライバのダウンロード

ここからダウンロードしてくる。
2021年9月時点での最新版はojdbc10.jarらしいが、これだと色々なドキュメントに載っている方法で接続できなかった(正しくドライバが読み込めなかった)ので、ojdbc8.jarを使う。
tar.gzでダウンロードされるので、プログラムと同階層に配置して解凍する。

(1)-2 プログラムの修正

DBのURL部分を以下のように記載変更する。

...(略)...
            String url = "jdbc:oracle:thin:@xxxx_high?TNS_ADMIN=D:/test/";
            String username = "ADMIN";
            String password = "xxxxx";
...(略)...
  • urlの書式はjdbc:oracle:thin:@[接続文字列]?TNS_ADMIN=[tnsnames.oraが格納されているディレクトリ]である。
    「tnsnames.oraが格納されているディレクトリ」はウォレットファイルを解凍した場所のフォルダを指定すればよい。
    tnsnames.oraにはhigh、low、mediumの3つの接続定義が載っているので、「接続文字列」はその中から一つを選んで記述する。
    詳細はこの辺のDocに載っている。
  • usernameはADMINで固定でいい。パスワードはウォレットファイルをダウンロードしたときのパスワードを記述する。

また、些細なことだが、実行するSQLを以下のように変更する。

...(略)...
            //ps = con.prepareStatement("SELECT 1");
            ps = con.prepareStatement("SELECT 1 FROM DUAL");
...(略)...

OracleではSELECT 1が通用しない(必ずFROM句が必要になる)のでとりあえずdual表を付けておく。

上記の修正を施したプログラムとして「Connect2OracleTest.java」として保存する。

(3) クライアント側プログラム実行

コンパイルは例によってjavac Connect2OracleTest.javaで通る。
しかし実行に際しては、他2DBと違って色々クラスパスに含める必要がある。
具体的には「ojdbc8.jar」「ucp.jar」「oraclepki.jar」「osdt_core.jar」「osdt_cert.jar」の5ファイルである。
これらの5ファイルは上記(1)でDLしてきたファイルを解凍した中に全部含まれている。
これらの5ファイルが格納されているディレクトリを「ojdbc8-full」だとした場合、実行に際してクラスパスの設定をすると以下のようになる。

コマンドプロンプトだと

> java -cp .;.\ojdbc8-full\ojdbc8.jar;.\ojdbc8-full\ucp.jar;.\ojdbc8-full\oraclepki.jar;.\ojdbc8-full\osdt_core.jar;.\ojdbc8-full\osdt_cert.jar Connect2OracleTest

Powershellだと

> java -cp ".;.\ojdbc8-full\ojdbc8.jar;.\ojdbc8-full\ucp.jar;.\ojdbc8-full\oraclepki.jar;.\ojdbc8-full\osdt_core.jar;.\ojdbc8-full\osdt_cert.jar" Connect2OracleTest

Linuxだと

> java -cp .:./ojdbc8-full/ojdbc8.jar:./ojdbc8-full/ucp.jar:./ojdbc8-full/oraclepki.jar:/ojdbc8-full/osdt_core.jar:./ojdbc8-full/osdt_cert.jar Connect2OracleTest

(4) セッション切断

セッションを探す。

select sid, serial#, status, to_char(logon_time,'yyyy/MM/dd HH24:mi:ss') logon_time from V$SESSION where module = 'JDBC Thin Client'

上記で取得した「SID」「SERIAL#」をもとに以下のSQLを実行する。
例えばSIDが100、SERIAL#が200だったら以下のようになる。

alter system kill session '100,200' IMMEDIATE;

(5)結果発表おおおおお!!

(A)コネクション繋いでからSQL発行する前に切断

PreparedStatement#executeQueryで落ちる。
ついでにその後のPreparedStatement#close及びfinally句のConnection#closeでも落ちる。
試した中では一番厳密に色々落ちる。

start connection
start waiting 60 seconds
start creating PreparedStatement
start executing query
java.sql.SQLRecoverableException: IOエラー: Connection closed
        at oracle.jdbc.driver.T4CPreparedStatement.executeForDescribe(T4CPreparedStatement.java:821)
        at oracle.jdbc.driver.OracleStatement.executeMaybeDescribe(OracleStatement.java:983)
        at oracle.jdbc.driver.OracleStatement.doExecuteWithTimeout(OracleStatement.java:1168)
        at oracle.jdbc.driver.OraclePreparedStatement.executeInternal(OraclePreparedStatement.java:3666)
        at oracle.jdbc.driver.T4CPreparedStatement.executeInternal(T4CPreparedStatement.java:1426)
        at oracle.jdbc.driver.OraclePreparedStatement.executeQuery(OraclePreparedStatement.java:3713)
        at oracle.jdbc.driver.OraclePreparedStatementWrapper.executeQuery(OraclePreparedStatementWrapper.java:1167)
        at Connect2OracleTest.main(Connect2OracleTest.java:28)
Caused by: java.io.IOException: Connection closed
        at oracle.net.nt.SSLSocketChannel.readFromSocket(SSLSocketChannel.java:604)
        at oracle.net.nt.SSLSocketChannel.fillReadBuffer(SSLSocketChannel.java:330)
        at oracle.net.nt.SSLSocketChannel.fillAndUnwrap(SSLSocketChannel.java:260)
        at oracle.net.nt.SSLSocketChannel.read(SSLSocketChannel.java:111)
        at oracle.net.ns.NSProtocolNIO.doSocketRead(NSProtocolNIO.java:560)
        at oracle.net.ns.NIOPacket.readHeader(NIOPacket.java:263)
        at oracle.net.ns.NIOPacket.readPacketFromSocketChannel(NIOPacket.java:195)
        at oracle.net.ns.NIOPacket.readFromSocketChannel(NIOPacket.java:137)
        at oracle.net.ns.NIOPacket.readFromSocketChannel(NIOPacket.java:110)
        at oracle.net.ns.NIONSDataChannel.readDataFromSocketChannel(NIONSDataChannel.java:91)
        at oracle.jdbc.driver.T4CMAREngineNIO.prepareForUnmarshall(T4CMAREngineNIO.java:791)
        at oracle.jdbc.driver.T4CMAREngineNIO.unmarshalUB1(T4CMAREngineNIO.java:449)
        at oracle.jdbc.driver.T4CTTIfun.receive(T4CTTIfun.java:410)
        at oracle.jdbc.driver.T4CTTIfun.doRPC(T4CTTIfun.java:269)
        at oracle.jdbc.driver.T4C8Oall.doOALL(T4C8Oall.java:655)
        at oracle.jdbc.driver.T4CPreparedStatement.doOall8(T4CPreparedStatement.java:270)
        at oracle.jdbc.driver.T4CPreparedStatement.doOall8(T4CPreparedStatement.java:91)
        at oracle.jdbc.driver.T4CPreparedStatement.executeForDescribe(T4CPreparedStatement.java:807)
        ... 7 more
start colosing PreparedStatement
Exception in thread "main" java.sql.SQLRecoverableException: クローズされた接続です。
        at oracle.jdbc.driver.PhysicalConnection.needLine(PhysicalConnection.java:3550)
        at oracle.jdbc.driver.OracleStatement.closeOrCache(OracleStatement.java:1478)
        at oracle.jdbc.driver.OracleStatement.close(OracleStatement.java:1461)
        at oracle.jdbc.driver.OracleStatementWrapper.close(OracleStatementWrapper.java:122)
        at oracle.jdbc.driver.OraclePreparedStatementWrapper.close(OraclePreparedStatementWrapper.java:98)
        at Connect2OracleTest.main(Connect2OracleTest.java:44)

(B)SQL発行してからResultSet(とかConnectionとか)閉じるまでに切断

Connection#closeで落ちる。
ただしこれもPreparedStatement#closeは何事もなく普通に通り抜けるらしい。

start connection
start waiting 60 seconds
start creating PreparedStatement
start executing query
1
start waiting 60 seconds
start colosing PreparedStatement
start colosing Connection
Exception in thread "main" java.sql.SQLRecoverableException: IOエラー: Connection closed
        at oracle.jdbc.driver.T4CConnection.logoff(T4CConnection.java:1022)
        at oracle.jdbc.driver.PhysicalConnection.close(PhysicalConnection.java:2290)
        at Connect2OracleTest.main(Connect2OracleTest.java:48)
Caused by: java.io.IOException: Connection closed
        at oracle.net.nt.SSLSocketChannel.readFromSocket(SSLSocketChannel.java:604)
        at oracle.net.nt.SSLSocketChannel.fillReadBuffer(SSLSocketChannel.java:330)
        at oracle.net.nt.SSLSocketChannel.fillAndUnwrap(SSLSocketChannel.java:260)
        at oracle.net.nt.SSLSocketChannel.read(SSLSocketChannel.java:111)
        at oracle.net.ns.NSProtocolNIO.doSocketRead(NSProtocolNIO.java:560)
        at oracle.net.ns.NIOPacket.readHeader(NIOPacket.java:263)
        at oracle.net.ns.NIOPacket.readPacketFromSocketChannel(NIOPacket.java:195)
        at oracle.net.ns.NIOPacket.readFromSocketChannel(NIOPacket.java:137)
        at oracle.net.ns.NIOPacket.readFromSocketChannel(NIOPacket.java:110)
        at oracle.net.ns.NIONSDataChannel.readDataFromSocketChannel(NIONSDataChannel.java:91)
        at oracle.jdbc.driver.T4CMAREngineNIO.prepareForUnmarshall(T4CMAREngineNIO.java:791)
        at oracle.jdbc.driver.T4CMAREngineNIO.unmarshalUB1(T4CMAREngineNIO.java:449)
        at oracle.jdbc.driver.T4CTTIfun.receive(T4CTTIfun.java:410)
        at oracle.jdbc.driver.T4CTTIfun.doRPC(T4CTTIfun.java:269)
        at oracle.jdbc.driver.T4C7Ocommoncall.doOLOGOFF(T4C7Ocommoncall.java:64)
        at oracle.jdbc.driver.T4CConnection.logoff(T4CConnection.java:1011)
        ... 2 more

結果まとめえええええええ!!!

各DBに対して、セッションを切ったタイミング

  • (A)コネクション繋いでからSQL発行する前に切断
  • (B)SQL発行してからResultSet(とかConnectionとか)閉じるまでに切断

でどうなったかをまとめると、以下のようになる。

DB 利用したJDBC (A) (B)
Postgresql postgresql-42.2.23.jar org.postgresql.util.PSQLException おちない(正常終了する)
MySQL mysql-connector-java-8.0.26.jar com.mysql.cj.jdbc.exceptions.CommunicationsException java.sql.SQLNonTransientConnectionException
Oracle ojdbc8.jar java.sql.SQLRecoverableException java.sql.SQLRecoverableException

実行環境は以下の通り。

software version
Java java version "1.8.0_271"
Docker Docker version 20.10.8, build 3967b7d
Postgresql PostgreSQL 13.3 (Debian 13.3-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
MySQL 8.0.26
Oracle Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production
  • もっといろんな種類のDBを見ていくと傾向が違ってくるのかもしれないが、とりあえず試した範囲ではポスグレだけがちょっと違っている。他の2DBは(B)で切ったら死ぬが、ポスグレだけは(B)で切っても死なない。
  • MySQLの(A)やOracleの(A)(B)などはいろんなExceptionがネストされているが、面倒くさいので一番最初のExceptionのみ上記の表には記載している。
  • MySQLとポスグレは、少なくとも(A)の段階で切ったら、自前の例外を投げているのに対し、Oracleは、(A)でも(B)でも、投げられる例外はjava.sql配下にあるもののようだ。自前の例外ってのを定義してないのかもしれない。
  • いずれのDBでも(A)の場合はPreparedStatement#executeQuery及びConnection#closeで落ちる。これは共通している。(今さらだが、PreparedStatementは、単なるStatement#executeQueryだと結果が違ったかもしれない。この辺、若干興味はあったが、試していない)
  • PreparedStatement#closeは落ちるケースと落ちないケースが混じっている。ただ、試した範囲内ではOracle の(A)でしか落ちておらず、ポスグレとMySQLは(A)でも(B)でも落ちない。つまり、基本的に落ちない(落ちづらい)ようになってるらしい。

いずれにしてもJDBCの実装に依存する部分だと思われるので、JDBCのバージョンが違えば挙動も変わってくるだろう。
また、今回、実行しているのがトランザクションにならない単発のSELECTだけというのも恐らく結果に影響している部分はあると思う。
正直ポスグレとMySQLはあんまり詳しくなく、その辺の絡みでなんか結果に変化ありそうだな、と薄々思ってはいたが、まずSELECT 1すらやったことないのにいきなりそんなのに挑戦するのは憚られたので、一旦単発のひじょうに簡易なSQLで試すだけにとどまった。
この辺は興味が湧いたらそのうち試す、かもしれない。

また、上に書いた通り、ポスグレとMySQLはDockerだが、OracleだけはクラウドDBを利用しており、それによる違いはもしかしたら出ているかもしれない。
同条件にしないと厳密には比較にならないのはその通りなのだが、OracleをDockerでたてる、というのが凄まじく億劫でやる気をそがれ、少なくともこの実験のためだけにやろうという気にならなかった。
まあ、これも気が向いたらそのうち。。。

おわりに

いくつかの種類のDBに対するクライアント側のプログラムの作成、久しぶりにJavaやって勘を取り戻せたこと(特にクラスパスの設定方法w)、MySQL・ポスグレに対するセッションの探し方及び切断の仕方など、短時間に学ぶものはあって面白かった。
というかむしろこの実験はそれらを効率よく学習するための素材だったに過ぎず、結果はそれの副産物だと思っている。
面白い実験だったので、こういう視点の実験は今後も可能なら続けていきたいと思う。
まあでもそこそこ時間がかかるので暇なとき限定になってしまうだろうけどw