HiveServerでファイルディスクリプタがリークする件について

@tagomorisさんがこんな事を言っていたので、調べてみました。

まずは、.pipeoutファイルについて。
結論から言うと、Hiveの不具合です。
((追記 2012/06/23 14:00)Hiveのプログラムの書き方は良くないんだけど、Leakはしていないと思われます.)

この.pipeoutファイルはSessionStateのstartメソッドでOpenされます。startメソッドはHiveServerの場合、クライアントがコネクションをはった時にコールされます。(HiveSourceCodeReadingの発表資料にもちょっとだけ書いてあります。)

.pipeoutファイルはHiveServerではpipeInという変数によって、BufferedReader -> FileReaderを通して参照されます。 ↓のような感じ。
pipeIn = new BufferedReader(new FileReader( .pipeoutファイル ))
後始末の処理として、pipeIn = null としています。これがNGです。closeしていません。 

GCによってcloseされないのか?と思うかもしれません。が、されません。
GCによってcloseされるのはFileInputStreamだけ(あとはFileOutputStream)です。
FileInputStreamはfinalizeでcloseする事を仕様として定めていますが、FileReaderは仕様として定めていません。FileReaderの親クラスであるInputStreamReader, Readerともに仕様にありません。
JDK6のソース(OracleJDK, OpenJDK)を見ても、finalize処理は実装されていません。
※ちょっと嘘書きました。末尾に追記しました。(FileReaderはコンストラクタでnew FileInputStreamしていますのでGCでcloseされます)

ちょっと横道にずれますが、.pipeoutファイルってdeleteOnExitしているんですよね。このファイルは毎回異なるファイル名で作成しているので、コネクションをはる度に解放されないオブジェクトが溜まっていきます(まぁメモリリークしているわけですが、定義的にメモリリークと言って良いのかわかんない)。 HiveServerのclean処理で.pipeoutファイルをdeleteしても、deleteOnExitのリスト(正確にはLinkedHashSet)からは消えないままです。

というか、openしたままdeleteできるかどうかってプラットフォーム依存だった気がしますが、もう直ってるんでしたっけ? BugID忘れた&調べる気ない(・x・)

次に.txtファイルについて。
これってLog4jで出力しているファイルの事なのかな?(デフォの設定だと、.logなので違うファイル?)
こっちはまだ調べていませんが、Log4jのFileAppenderの場合、ファイル名を設定する度に1つ前に開いていたFile(というかWriter)をcloseする処理が入っているところまでは確認済み。HiveのExevDriverで以下のように書いていて、これってFileAppenderのreset処理呼ばれるっけ・・。と疑問に思ったところで、こっちはいったん調査終了。

    System.setProperty(HiveConf.ConfVars.HIVEQUERYID.toString(), HiveConf.getVar(conf,
        HiveConf.ConfVars.HIVEQUERYID));
    LogManager.resetConfiguration();
    PropertyConfigurator.configure(hive_l4j);

たぶんこっちの事かな・・。 hive_job_log_(sessionId)_(乱数).txt
こっちの場合は、HiveHistoryというクラスが担当で、PrintWriterを介してファイルをOpenしており、close処理がfinalizeで呼ばれています。このHiveHistoryはSessionStateのstartメソッドでインスタンス化されます。

SessionStateはThreadLocalに保存されます。HiveHistoryはSessionStateが保持しています。HiveHistoryは様々な場所から参照されていますが、他のどこにも保持はしていません(インスタンス生成時にSessionStateを受け取っているので、一見循環参照か!? と思ったけど保持はしていないです。)。Thriftがどういうモデルなのか分からないですが、仮に接続毎にスレッドを生成しているのであれば、接続がきれた時にSessionStateがThreadLocalから消えて他のどこからも参照がない場合(これはまだ確認してない)、SessionStateはGCの対象になります。その時にHiveHistoryへの参照が全て無くなるので、GC対象となるはずです。

仮定が多くて調査になっていないですね(=w=;)
jmapとjhatでどういう状態になっているか確認すれば良いだけなのですけどね。

お腹すいたのでいったん調査終了(・ω・)

※(追記)
当たり前の事なのでわざわざ書いてませんでしたが、一応捕捉しておきます。
(少なくともJavaでは)openしたものはきちんとcloseすることを明示すべきですし、
別にGCまかせにすることを推奨とも良しともしていないです。(そんな事一言も書いてないですよね(>ω<))
原因となりうるかと、それを良しとするかは軸が違うと思うのです。

(さらに追記)

FindBugsで解析をすると分かるらしいです。
今度試してみます。

ふむふむ。tagomorisさんのtweetによると、CDH3u2との事なので丁度ぶちあたってしまうのですね。
あと、確認したソースのバージョンを明記してなかったです。trunkとCDHu4で見てました。

(追記)2012/06/23 13:00

FileReader(File)はコンストラクタでsuper(new FileInputStream(file)) としていました。
という事で、GC走ればcloseされます。 
簡単な確認テスト: https://gist.github.com/2976972 

新しいサイトもよろしくお願いします!