いくつかの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#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.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