JNDIで認証を行っているアプリがあり、通常利用している分には問題ないのだが、負荷がかかるとエラーになるという現象が出ていて、その調査を行った。
現象としては、いっぺんにある程度のユーザーがログインする状況が続くとエラーが出る、というものだった。
はじめは、LDAPの接続上限かなんかだと思いリトライ処理を入れて様子を見たのだが、どうにもよくならない。
で、負荷をかけながらGCのログをみていると、FullGCが走った直後に接続に成功するというパターンに気がついた。
もう何年も動いているアプリなので開放漏れとかはないだろうと思ったが、現象が昔よくやらかしたConnectionの開放漏れの時に状況が似てるなと思い、LDAP接続のclose呼び忘れ部分を調べてみたのだが、やはりコード上で漏れている様子はなかった。
困ったところでGoogle先生を検索したところ、
http://www-1.ibm.com/support/entdocview.wss?rs=180&context=SSEQTP&q1=JNDI-LDAP&uid=swg21191219&loc=en_US&cs=utf-8&lang=en&NotUpdateReferer=
にたどり着いた。
キモの部分だけ抜粋。
Bad CASE DirContext ctx = new InitialDirContext(env); // Search for objects with those matching attributes NamingEnumeration answer = ctx.search("","(objectclass=*)", ctls ); // Close the context when we're done ctx.close(); Good CASE DirContext ctx = new InitialDirContext(env); // Search for objects with those matching attributes NamingEnumeration answer = ctx.search("","(objectclass=*)", ctls ); answer.close(); // Added to close enum instance!!!! // Close the context when we're done ctx.close();
NamingEnumerationというのがJDBCでいうResultSetに当たる部分。
で、アプリではこのNamingEnumerationを取得した後、DirContextのclose()を呼んでそのままNamingEnumerationを返す、という実装をしていた。
要約すると、
public NamingEnumeration find(String dn){ DirContext ctx = new InitialDirContext(env); NamingEnumeration answer = ctx.search("",dn, ctls ); ctx.close(); return answer; } (除く例外処理)
という感じの実装。
このメソッドを呼び出したクラスが、戻り値のNamingEnumerationからデータを取り出して処理をするという流れになっていたため、NamingEnumerationのcloseを呼び出す事をしていなかった。
おそらくNamingEnumerationというのを、単に結果の入ったリストと思っていたのと思われる。
(取得元のDirContextをcloseしたあとに普通に結果が取れるから、closeメソッドがあることすら知らなかったのかもしれない)
このためうまく接続が開放されなかった、というオチ。
わりと多くの部分でこのメソッドを呼んでいたため、結局次のような感じで乗り切ってもらうことにした。
public NamingEnumeration find(String dn){ DirContext ctx = new InitialDirContext(env); NamingEnumeration answer = ctx.search("",dn, ctls ); List dataList = new ArrayList(); while (answer.hasMore()) { SearchResult sr = (SearchResult) enumImp.next(); dataList.add(sr); } DummyNamingEnumeration dummyEnum = new DummyNamingEnumeration(dataList); answer.close(); ctx.close(); return dummyEnum; } (除く例外処理)
DummyNamingEnumerationはNamingEnumerationを実装しており、hasMore()とnext()の呼び出しに対して、引数のdataListのIteratorの該当メソッドに処理を委譲する、というコードになっている。
ひとまずこの実装をしてやることで、きれいに接続が切れるようになった。
InitialDirContextの実装クラス(IBM製)にはおそらくfinallizeメソッドが実装されていて、GCタイミングで接続を開放するようなコードになっており、FullGC時に接続が破棄された分が新たに接続できるようになったので、GCに絡んだ挙動になったのだと思われる。
解決してみるとなんのことはない話ではあったが、大量に接続してもスパッと切断される様をみれて、ちょっとスカッとした。