タグ別アーカイブ: java

StringBufferとStringBuilderの違い

javaには文字列を可変的に扱うためのクラスとして StringBuffer 及び StringBuilder という2つのクラスが用意されています。
この2つのクラスは文字をバッファとして取り扱い、任意のタイミングで任意の文字列を詰め込むことができ、非常に使用頻度の高いクラスとなっています。
されとてこの2つのクラス同じような事できるのですが具体的に、どう異なるかご存知でしょうか?
本記事ではこの2つのクラスの差について調査します。

なお対象としては java8 の API を対象とします。

調査する

公式の java8 API を参照してみましょう。参考リンクについては本記事の末尾に記載しますので参照してください。

と、いきなり回答にたどり着いてしまうのですが StringBuilder のマニュアルを確認すると、以下のような記載があります。

文字の可変シーケンスです。このクラスは、StringBufferと互換性があるAPIを提供しますが、同期化は保証されません。このクラスは、文字列バッファが単一のスレッド(一般的なケース)により使用されていた場合のStringBufferの簡単な代替として使用されるよう設計されています。このクラスは、ほとんどの実装で高速に実行されるので、可能な場合は、StringBufferよりも優先して使用することをお薦めします。

つまり StringBuilder はスレッドセーフではないシンプルなケースに適用でき、それ以外の場合は StringBuffer を使用すると良い、ということです。

シンプルに使い所をまとめると

StringBuffer

スレッドセーフであるので複数スレッドから参照されるような場合に適切
スレッド間の排他制御を実装している。

StringBuilder

シングルスレッドで排他処理の必要がないシンプルな処理の場合に適切
使用できる場合はこちらのクラスを使用したほうがStringBuilderよりも高速に処理が行える

というところですね。

実験してみる

実装レベルでの検証のために下記のようなサンプルコードを用意しました。
ちなみに groovy コードとなりますのでご留意ください。

同じ文字列バッファを共有したスレッドを10個作成してそれぞれのスレッドから100回書き込みを行います。
最終的にバッファの内容がどうなっているかを確認するシンプルなコードです。
これを文字列バッファをStringBufferとStringBuilderそれぞれに切り替えて結果を確認してみます。

import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

class WriteThread implements Runnable {
  int num
  StringBuffer sb

  public WriteThread(int num, StringBuffer sb) {
    this.num = num
    this.sb = sb
  }

  @Override
  public void run() {
    for (int i=0; i<100; i++) {
      this.sb.append("I am thread ${num}.\n")
    }
  }
}

StringBuffer sharedBuilder = new StringBuffer()
ExecutorService executor = Executors.newFixedThreadPool(10)
for (int i=0; i<10; i++) {
  executor.submit(new WriteThread(i, sharedBuilder))
}
executor.shutdown()
executor.awaitTermination(10, TimeUnit.SECONDS)

println sharedBuilder.toString()

StringBuffer

結果の一部のみを抜粋しますが、きれいに文字列が出力できていることが確認できます。

...
I am thread 6.
I am thread 1.
I am thread 3.
I am thread 7.
I am thread 3.
I am thread 1.
I am thread 6.
...

StringBuilder

対して StringBuilder の方は下記のように破損している箇所が見られます。
(※見れられないこともあります)
書き込みが衝突してしまったということです。

...
I am thread 7.
I am thread 9.
ead 8.
thread 9.
ad 1.
ad 0.
I am thread 5.
...

まとめ

とりあえずマルチスレッドなケースが求められる場面では StringBuffer を使いましょう。
にしても名前がややこしいな・・・どっちがどっちだかわからなくなる。

参考

StringBuilder
StringBuffer


時間が立つとGoogle APIのOAuth認証に失敗する

概要

Googleが提供するツール。便利ですよね。
公式ではよくG Suitesと呼ばれているスプレッドシートや、ドキュメントなどなどのことを指します。
アプリケーションからG Suitesを操作するためにはOauth認証を利用するのですが、今回はその際時間が立つと認証時にエラーが出てアプリケーションからの操作ができなくなる問題に遭遇したのでまとめておきます。

エラー詳細

今回のアプリケーションはG Suitesの操作をアプリケーションで実行しています。
挙動としてはまずOAuthクライアントを作成したユーザで 認証を行い、アクセストークンを取得します。
その後アクセストークンを用いてG Suitesの操作を行うようなごくごくシンプルなものです。

当初はアプリケーションが正常に動作するのですが、一定時間が経過した上で再度実行するとG Suitesの操作を行う際にトークンが正常でないエラーが発生するような現象が起こりました。

もう少し細かい実装について述べましょう。
今回はgoogle側で用意してくれているjavaのライブラリを使用しています。
かなりライブラリが充実しているので普通に考えてこれを使わない手はありません。

認証には公式ガイドに沿った典型的な実装だと思います。

GoogleAuthorizationCodeFlow を利用して認証しアクセストークンを取得します。
また認証した情報は FileDataStoreFactory を用いてローカルディスクに保存します。
一度認証した後はセッションを発行して、二度目以降は保存したトークンを用いてアプリケーションの処理を行います。

原因

一定時間が経過すると再度実行できなくなるということから、おそらくトークンが期限切れになった際にトークンのリフレッシュができていないんじゃないかと当たりをつけました。
ちょうどはじめに返却されるアクセストークンの生存期間が1時間でしたので、期限が切れたタイミングで実施するとやはり、エラーが再現することが確認できました。
更に認証時に返却される情報をよくよく確認してみると、リフレッシュトークンが返却されていません。
なのでFileDataStoreにはアクセストークンなどは保存はされているんですが、当然リフレッシュトークンがないのでアクセストークンの期限が切れた瞬間に再発行ができずにエラーとなっているようです。

今回はリフレッシュトークンが空っぽいという点に気づくのに時間がかかりました。
あとはデバッグする際にはアプリケーションを再起動して確認していたので、初回のアクセストークンが切れる前に再度アクセストークンが新しいものに変わるんですよね。なのでエラーが出るまでに時間がかかるので気づきにくかったです。

調べてみますとこちらに情報があり、リフレッシュトークンは初回の認証した時点でしか返却されないそうです。
なんですが approval_promt=force を指定することで毎回認可ダイアログに飛び、それによってリフレッシュトークンが返却されるようになります。

いろいろ試してみたところ、途中から認可ダイアログが表示されなくなるユーザ(この場合リフレッシュトークンが帰ってこなくなります)もいれば、ずっと認可ダイアログが開くユーザもおり、正直なところ何を基準にそうなっているのかわかりませんでした。

参考URLのようにパラメータを指定することで強制的に認証ダイアログを表示するなり、シークレットをリセットするなりするのが対応としては良さそうです。

参考

https://stackoverflow.com/questions/10827920/not-receiving-google-oauth-refresh-token


gradle daemonを無効にする

javaのバックグラウンドを長く持つ自分にとって最近のお気に入りの言語はgroovyです。

もともと自分は言語の習得にコストを掛けたくなく、新しい言語が少なからず生み出される現状に少なからず嫌悪感を持っているところがなくはないです。
歴史的な経緯で高級言語が生み出されるようなことは好ましいのですが、同時代に構文だけが異なる似たような言語や、今ままでの低級言語のAPIから何故かひどく乖離した構文を提供する言語は疎ましくも思います。

もともとjavaは広く普及していたので自然と使わざるを得なかったのですが、広く使われているだけあってかなり洗礼されています。
javaはやはりLLと比較した時に型付き言語であるためコンパイル時点でかなり品質を担保できます。
またそれでいてCやC++などのレガシーな言語ほどコストを掛けずに開発できることから自分の中では攻撃力/防御力ともに優れた言語、と言った印象でした。
もっというと優れたIDEがあるのがよいですね。

なんですが欲を言うとjavaはその防御力の高さや歴史的には比較的古くから生み出された故にそのAPIに一部不満がある点もなくはないのです。
java8などの新しいAPIでは新しい流れをかなり汲み取っているようで不満があるかというとそういうわけでもないですが、いかんせん他の流れが早すぎるのだと思います。
新しい言語と比べると記述がかったるいことがあります。

このgroovyという言語はjavaというバックエンドを全く損なうことなく、実装速度をぐっとお仕上げてくれるようなものです。
というわけですごく気に入っているのです。

が、良いことばかりかなというと深い部分に行ったりするとちょっと不明な挙動を示したりすることもあるんですが、今回取り上げるのは gradle daemon と呼ばれる仕組みです。

groovyは様々なビルドシステムを利用することができますが、その中でも有名なものの一つにgradleと呼ばれるものがあります。
gradleはgroovyで開発されており、javaやgroovyのビルドツールとして利用できます。

ビルとツールによりgroovyソースはコンパイルされjavaのclassファイルへと変換されたりと様々な事が行われるのですが、その際に多くのオーバーヘッドを含みます。
そのためgradleではそのビルドコストを少なくするためにgradle daemonという仕組みを提供しています。
このdaemonは常駐することでビルドにかかるコストを削減してくれます。

今回この gradle daemon によりちょっとした不都合が起こりました。
現在個人として開発中のシステムで、ものすごくメモリを食うプロセス(javaではない)を起動しなければなく、しかしメモリを食いすぎるゆえに oom-killer によってプロセスが殺されることが続きました。

はじめはそのプロセスのメモリの消費量を抑えることを考えていたので、それが非常に困難であることがわかり、発想を変えて他のプロセスの精査を行うことにしました。

で、TOPコマンドで見てみるといるじゃないですか。でかいのが。

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
15032 z   20   0 3495m 433m  14m S 130.3 23.1   1:09.40 java

上記のプロセスの実態は gradle daemon です。

メモリを大量に消費していますね。物理メモリのおよそ25%をgradle daemonが抱えていることになります。
上記はコンパイル時の状態なんですが、daemonはその目的達成のために各種リソースを抱えたまま常駐します

これはとんでもないということで即見直しの対象として検討しました。
使用されなければswapに吐き出されるんですが、swapに吐き出されるとしてもswapを消費してしまって結構邪魔だし、そこまで頻繁にビルドしない。
そして結局 gradle daemon も oom-killer に殺されているという始末。
というわけでdaemonを起動しないようにしました。

実装としては gradle.properties に下記のように記述を加えました。

org.gradle.daemon=false

これにより無事にメモリを確保することができ、目的のプロセスを起動することができました。

gradleは便利さ故に色々できるのですが、マニュアルがかなり膨大です。
しかも失礼ですが和訳は結構わかりにくいです。おそらく、原文を読んだほうが良いでしょう。

便利なものですが気をつけて使わないとトラブルの温床となりますね。


javaのクラスローダの仕組みについて

javaのクラスローダに関して下記のようなコードが存在するときの内部動作がよくわからなかったので調べ所を残すことにしてみました。

Class clazz = App.class;
ClassLoader cl = clazz.getClassLoader();

クラスとクラスローダについて

さてこの getClassLoader というメソッドは何なのか、ということですが文字通りクラスローダを取得することができます。
このメソッドは Class クラスのメソッドですが、例えばアプリケーションを実装していて同プロジェクトに B というクラスと C というクラスが作成したときに下記のようなコードが存在したときに、この違いはあるんでしょうか?

ClassLoader clB = B.class.getClassLoader();
ClassLoader clC = C.class.getClassLoader();

先に答えを言ってしまうと、この2つで返却されるクラスローダは同じものになります。

理由ですがこの getClassLoader() を呼び出すとクラスは自分の参照するクラスローダを返却します。
すべてのクラスは必ず一つのクラスローダへの参照を持っており、それは自クラスをロードしたときに使用されたクラスローダを参照するようになっています。

そのため B, C は同プロジェクトのアプリケーションとして実装されているので、この2つは同じクラスローダによってロードされるため上記のような場合は大抵は同じクラスローダが返却されます。

クラスローダによるクラスの解決について

またクラスローダは自分の解決するクラスの領域というものが決まっており、階層構造を持ちます。
これはDNSで各ネームサーバが自分のゾーンだけを管理して、解決できない場合は親ネームサーバへと処理を移譲する仕組みと同じようなものです。

javaのデフォルトパッケージを解決するクラスローダのことを「ブートストラップクラスローダ」と呼び、これは一番親のクラスローダとして位置します。
さらにその下にアプリケーションのクラスを解決するための「システムクラスローダ」というクラスローダが存在します。
そんなに複雑でないシステムの場合はこの「システムクラスローダ」がアプリケーションクラスのクラスローダとして機能すると考えていて問題ないと思います。

クラスローダはクラスロードの依頼を受けた際にはまず親のクラスローダに対して解決を移譲します。
もしクラスが解決できないようであれば自分自身の管理する空間を探索します。

例えば下記のようなサンプルコードを用意して実行してみましょう。
ちなみにこのコードはgroovyにて記述してあります。

class Main {
    static void main(String[] args) {
        ClassLoader cl = Main.class.getClassLoader()
        println cl
    }
}

この実行結果は下記のように出力されました

sun.misc.Launcher$AppClassLoader@18b4aac2

「システムクラスローダ」というのは一般的な機能に対する名称のようで、クラス実態としては sun.misc.Launcher.AppClassLoader というクラスが実態のようです。
通常はこのシステムクラスローダがすべてのアプリケーションクラスのクラスローダとして機能します。

異なるクラスローダが用いられるアプリケーションはあるのか

主にJ2EEサーバなどにおいてはwarをおいただけで異なるアプリケーションをホストできる仕組みを提供しているため、各種ウェブアプリケーション単位でシステムクラスローダが異なるような形になると思われます。
設置した複数のwarファイルの中で同じ名称のクラスが存在する際に、うまく解決できないようではアプリケーションとして致命的ですので、このような場合にうまく機能してくる仕組みだと思われます。

通常はひとつのプロジェクトが固有の一つのクラスローダを持つ、、的な理解で問題ないと思います。

参考ページ

https://docs.oracle.com/javase/jp/8/docs/api/java/lang/ClassLoader.html
http://www.nminoru.jp/~nminoru/java/class_unloading.html


javaアプリケーションのcron起動時の文字化けに対応する

概要

javaアプリケーションの起動を行った際に、ある部分で文字化けが起こったので対応について調査する。

具体的には不具合が起こった箇所はjavaアプリケーションから外部プロセスを起動する部分や、メールを送信する機能の本文にVelocityのテンプレート機能を利用しており、これらを利用する箇所でそれぞれ日本語箇所が文字化けしていた。

原因

まず注目したいのは文字化けが起こっているケースがcronによるバッチからの起動に限定されることである。
バッチとは別にwebプロセスも動作しており、これはfat jarを作成してjavaコマンド経由でbashインタプリタ経由で起動している。が、こちらのwebプロセスは特に問題はない。

また先程メールが文字化けすると述べたが、実は文字化けするのは本文だけで、タイトルに関しては文字化けしていなかった。両者の違いは本文は外部テンプレートファイルから読み込んでいるのに対して、タイトルはソースコードに日本語をハードコーディングしている。

このことから大きく予想できるのはcron経由で外部ファイルを読み込むときに何らかの不具合が生じていることである。
cronはenvなどが特殊なんだろうかと当たりをつけて軽くウェブを検索するとこちらの記事を発見

どうやらcron経由でのコマンドはユーザの環境変数は一切適用されない模様。

解決

わかってしまえばなんてことはない。今回は参考サイトに則ってcrontabの記述の上にenv設定を行うことにします。
今回の場合はjavaがおそらく外部ファイルの文字コードを認識できていないのでcrontabの上部に下記のようにファイルエンコーディングを追記しました。

_JAVA_OPTIONS=-Dfile.encoding=UTF-8

無事にアプリケーションが動きましたとさ


javaにおけるURLとURIの違い

javaにおいて似たような(これはわかってる人からしたらおしかりを受けそうな表現ではあるが)URLとURIについて、実は結構その違いについてうやむやにしたままやってきた部分があるので、この際ですしその違いについて調べてみます。

概要

javaにおけるURL(java.net.URL)と URI(java.net.URI)の違いとかについて述べます

調査

調べましょう。と言っても実はjavaDocsにかなり詳細が書いてありますのでそちらを参照していただけば早い話です。

URLおよびURIを確認しましょう。

かいつまんで解説すると

両者を比較しながら解説しましょう。
※下記の内容は javaの公式 に含まれる内容なのでより正確な情報を確認したい方は参照元を確認してください。

URI(Uniform Resource Identifier)はその名の示すとおり、リソースが指す場所を一意に示すための識別子です。

これには当然おなじみの http://example.com/hoge/fuga.html などのウェブ上のURLなんかを含みます。
幾つか例を上げてみましょう。

http://java.sun.com/j2se/1.3/
docs/guide/collections/designfaq.html#28
../../../demo/jfc/SwingSet2/src/SwingSet2.java
file:///~/calendar

でこれをちょっとした表現で示すと下記のようになっています。

[スキーム:]スキーム固有部分[#フラグメント]
ここで、角括弧 […] は省略可能なコンポーネントを表し、文字 : と # はその文字自体を表します。

さらにスキームが指定してあるものを 絶対URI と呼びます。また絶対でないURIは 相対URI と及びます。
さらにさらにスキーム固有部分がスラッシュで始まらないものを 不透明URI と呼びます。

不透明URIには例えば下記のようなものがあります

mailto:java-net@java.sun.com
news:comp.lang.java
urn:isbn:096139210x

とまあ、こういったようにリソースの識別子を文字列で表現したものがいわゆる URI となります。
不透明URIという表現が示すように、割りと固有の表現もありますが、そこら辺はあくまでそのドメインが解釈するものとして「これもURIですよ!」みたいな感じで(割りと)ゆるい定義をしています。

というわけで上記の定義に則っていればなんだってURIになります。
java実装面の話をしますとURIは単なるリソースを識別するための文字列なんで、それ以上それ以下でもありません。

URI の役割は、あるURIと他のURIが指しているものが一緒かどうか?とか、あるベースURIとある相対URIをくっつけて(これを解決するといいます)新しいURIを作成するとか、そういう責務がメインになります。

それに対して URL はもっと具体的なことを表現します。
Uniform Resource Locator という名前が指すように、何かしらのリソースが存在することを表現します。

またまたjava実装の話になりますが URL は URI で行っていた単純な文字列表現だけでなく、そのリソースにアクセスする手段などを合わせて提供します。
もっというとjavaのURLクラスに関しては ウェブスキーム(http)限定のリソースを表現する実装として機能しているようです。

やや抽象的な感じも残りますが、より詳細が知りたい方は合わせて

RFC2396RFC2732 を参照してみてください。


SeleniumのStaleElementReferenceExceptionに対応する

概要

Selenium (javaAPI) を用いてスクレイピングを実施しするような仕組みを作っていたところタイトルにもあるエラーに遭遇した
(そうでなくてもSeleniumはなれるまでなかなかピーキーな動きをするときもあるが)

厄介なことに毎回必ず同じタイミングで発生するわけでもなく、その発生はかなり不定期である。
実施しているスクレピング処理においては、条件を引き起こすページすら一致せずにどのページでもまんべんなく発生する可能性が存在するようである。

今回はその解決方法について調査して実装までを検討する。

調査

まずはスタックトレースを見てみる、関連部分だけを抽出すると概ね下記のようなメッセージが出ている。

Caused by: org.openqa.selenium.StaleElementReferenceException: Element not found in the cache - perhaps the page has changed since it was looked up
Command duration or timeout: 7.64 seconds
For documentation on this error, please visit: http://seleniumhq.org/exceptions/stale_element_reference.html
Build info: version: 'unknown', revision: 'unknown', time: 'unknown'
System info: host: 'something host', ip: 'xxx.xxx.xxx.xxx', os.name: 'Linux', os.arch: 'amd64', os.version: 'something os', java.version: '1.8.0_91'
Driver info: org.openqa.selenium.firefox.FirefoxDriver
Capabilities [{applicationCacheEnabled=true, rotatable=false, pageLoadStrategy=unstable, handlesAlerts=true, databaseEnabled=true, version=42.0, platform=LINUX, nativeEvents=false, acceptSslCerts=true, webStorageEnabled=true, locationContextEnabled=true, browserName=firefox, takesScreenshot=true, javascriptEnabled=true, pageLoadingStrategy=unstable, cssSelectorsEnabled=true}]
Session ID: xxxxxxxxxx

ご丁寧にドキュメントのURLがエラーメッセージに含まれているので、公式ページへアクセスしてみる。

読んでみるとこの例外が発生するパターンは大きく分けて2つ存在するようだ。


参照しているエレメントが削除された場合

大概はこちらのケースに該当するようである。
例えばエレメントの参照を取得した時点から、別ページに移動、その後さらに保持していた参照を引き続き利用しようとした場合や、はたまた何らかのjsライブラリによって保持していたエレメントの参照が別の参照に置き換えられてしまった場合などがある。

この場合例えばid属性などは同じものを保持しているのにもかかわらず、内部的には別のDOMインスタンスとして管理されているため、例えば同じ属性を用いてセレクタを作成し、参照を引っ張ってきたとしても以前参照していたものはなくなってしまったためこのような例外を引き起こす。

エレメントがDOMにアタッチされていない場合

例えばtabを表現するテクニックにDivを予め用意しておいて、実際に表示されるDomは一つだけで他のDomは単純に値を保持するためだけに使用されているような場合、場合によっては他のDivはDOMから参照が保持されていない場合があるようである。
こんなケースは非常にまれでしょう。


その他jsによってエレメントタイプが変更された場合などもこの例外を引き起こす可能性があるようです。
が、どれにしても今回のケースに当てはまっていないように思う。

そのためweb上を検索しほかのサイトを調査していたところ、ページが読み込まれる際に間髪入れずにDOMを検索する際にうまくいかないことがあるようである。
またリトライすることでうまくDOMを検索できるケースもあるようであった。

これらを踏まえて対応を考える。

対応

はっきりとした原因はわからなかったが、おそらく今回のケースはページを移動した直後にDOMを検索することが原因だと予想する。

そのためページを移動した後にwait処理を導入する。またそれと同時にリトライ機構を導入する。

実装

下記の用に実装を行った。概念的なものしか示していないがなんとなくイメージは掴めると思います。
もともとの処理としてはループで色々なページを探索していたので、forの概形だけ残してあります。

変更前

    for( someCondition ) {
        driver.get(url);

        // スクレイピング処理
        someScrape();
    }

変更後

    for ( someCondition ) {
        driver.get(url);

        // StaleElementReferenceException を回避するため、ここで明示的にDOMの読み込みを待つ
        _sleepSec(5);

        for(int count=1; count<=RETRY_COUNT; count++) {
            try {

                // スクレイピング処理
                someScrape();

                break;
            } catch (StaleElementReferenceException e) {
                // StaleElementReferenceException が発生した場合は規定回数内であればリトライを行う
                if (count >= RETRY_COUNT) {
                    throw e;
                }
                _sleepSec(5);
            }
        }
    }

対応したところ、今のところ問題なさそうである。

参考ページ

http://www.software-testing-tutorials-automation.com/2015/02/how-to-handle-stale-element-reference.html
http://blog.afnf.net/blog/69


scalikejdbcでmysqlとの通信時のCommunicationsExceptionに対処する

概要

scalaアプリケーションを作成している際に下記のような例外が発生しているケースがありました。

[CommunicationsException: Communications link failure

The last packet successfully received from the server was 15,048 milliseconds ago.  The last packet sent successfully to the server was 10 milliseconds ago.]

今回はこのエラーの原因とその対処についてまとめたいと思います。
ちなみにタイトルにはscalikejdbcと記述してありますが、原因分析についてはmysqlを使用していれば例外なくほぼ同じことが原因であることが多いです。
またアプリケーションについてもscalaといわず、javaを用いていれば同じような対応で解決できます。
(言わずともscalaってjavaのラッパーみたいなものなので)

また私の環境ではscalikejdbcというライブラリを使用したコネクションプールを使っており、検証にそれを用いることとしますし、特にその点については詳しく述べます。

原因

初めに原因だけ端的に記述しておきましょう。
原因としてはアプリケーションが確立したmysqlとの接続、いわゆる「コネクション」が途切れているにもかかわらずアプリケーションではその使えなくなったコネクションを利用してmysqlとの通信をしようとしているからである。

ネットなどで検索するとよくあるのが、一日立ってからアプリケーションが動き出そうとしたタイミングで一回だけ発生する。などの記述が少なくない。

これはアプリケーション(クライアント)が認識しているコネクションの有効性と、mysql(サーバ)が認識しているコネクションの有効性が食い違っているからである。

クライアントでは通常一度作成したコネクションに関してそのまま使い回す。
サーバでは一度作成されたコネクションをいつまでも有効にしていると、新しいクライアントとのコネションを作成する際の邪魔になってしまう。
そのため一定時間アイドル状態(通信が発生していない)であったコネクションをクローズする仕組みが存在する。

そのためサーバ側では閉じたはずのコネクションに対して、クライアントから通信が行われ、結果通信ができずエラーとなるのである。

コネクションプール

さて更にここで掘り下げてコネクションプールを用いている場合にはどうなっているのかということについてより詳しく記述する。
私の環境ではscalikejdbcというライブラリのコネクションプール機能を利用しているが、まあ他のアプリケーションでもコネクションプールを利用している場合はそんなに変わらないでしょう。

コネクションプールとはコネクションを確立する際のコストを減らすため、予め確立しておいたコネクションをすぐに使えるように準備する(プールする、という)仕組みである。

コネクションプールを用いた場合にも先の原因で説明したように全く同じことが言える。
プールされたコネクションは特に読み取り専用のコネクションであれば、一回使用した後もコネクションはクローズされずに何度も使いまわされる。
そして通信がしばらく行われなかった場合、プールされていたコネクションはサーバ側のタイムアウト設定により、使えなくなってしまうのである。

コネクションプールの設定にもよるが、プールされていたコネクションはサーバ側でコネクションが破棄されていたとしてもずっとプールされ続けることがある(というか特に設定しないかぎりは通常だとそうなる)
そのため同様にしばらく(サーバのタイムアウト設定以上)に通信が行われなかった後、再び実行しようとすると同じエラーを引き起こす。

対応

対応としてはいくつかあるが、適切なものとしては通信を行う前にコネクションの検証を行うsqlを発行することで回避できる。
scalikejdbcのような内部でDBCPを利用しているようなコネクションプールであれば、公式にサポートされている機能がある。

設定パラメータに存在するvalidationQueryという項目に適当なsqlを設定する。
例えばselect 1 as oneなど。
そうすることでコネクションを利用する前にその検証クエリを毎回発行するようになり、コネクションが切れていた場合は自動的に再接続を行なってくれるようになる。
コネクションプールを利用していない場合については、自身で実装を工夫する必要がある。

検証

ここまでの話を実装レベルで検証してみよう。

mysqlのタイムアウト値

mysqlの実装におけるコネクションのタイムアウト値はwait_timeoutという項目により設定される。
その項目を確認してみよう

show variables like 'wait_timeout';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| wait_timeout  | 28800 |
+---------------+-------+
1 row in set (0.00 sec)

デフォルトでは28800秒である。これは8時間に値する。
すなわちよくあるケースではアプリケーションが最後にデータベースと通信をおこない、その8時間以上経過してから再度データベースと通信を行うような処理を実行しようとしたところでエラーが発生する、というケースである。

検証する上で流石に8時間も待てないので、この時間を変更する

/etc/my.cnfを編集してタイムアウト時間を更新する
今回は下記のように設定することで10秒に指定した

wait_timeout=10

アプリケーションでエラーが発生することの確認

さて、ここでscalikejdbcを用いたplay scalaアプリケーションにて同エラーが発生することを確認する。
テスト用に作成したアプリケーションを用いることとする。
同じ状況が再現できれば、別にアプリケーションはなんでもよい。

application.conf

コネクションプールを利用するためdefaultという接続子を用いて下記のような設定を行う。

db {
  default.driver=com.mysql.jdbc.Driver
  default.url="jdbc:mysql://localhost/test"
  default.username=user
  default.password=password

  default.poolInitialSize = 10
  default.poolMaxSize = 10
}

アプリケーション

検証のため適当なコントローラに下記のような記述をする。
純粋にループしながらselectを行う。ループ中では15秒のwaitを入れる。

def index = Action {
  scalikejdbc.config.DBs.setupAll()

  var name: Option[String] = None

  (0 to 10).foreach(_ => {
    println("try read")
    name = DB readOnly { implicit session =>
      sql"select name from t order by id desc limit 1".map(rs => rs.string("name")).single().apply()
    }
    println(name)
    Thread.sleep(15000)
  })

  Ok("ok")
}

このアプリケーションを実行すると先のエラーが発生した。
予測はあっているようである。

さらに対応としてvalidationQueryを設定してみる。
scalikejdbcにおけるvalidationQueryの設定は下記のようになる。

db {
  default.driver=com.mysql.jdbc.Driver
  default.url="jdbc:mysql://localhost/test"
  default.username=user
  default.password=password

  default.poolValidationQuery="select 1 as one"
  default.poolInitialSize = 10
  default.poolMaxSize = 10
}

この設定を用いてアプリケーションを実行してみると、今度は無事にエラーが発生しないで実行できる。

またこの際のquery.logを確認してみる

...
2016-05-15T23:36:57.488754Z   71 Query	select 1 as one
2016-05-15T23:36:57.489167Z   71 Query	set session transaction read only
2016-05-15T23:36:57.489512Z   71 Query	SET autocommit=1
2016-05-15T23:36:57.490026Z   71 Query	select name from t order by id desc limit 1
...

目的のsqlの直前にvalidationQueryが存在していることが確認できる。
これによって無事に対応することができた。