ocamlのネットワークライブラリでfile_descrリークするものが多い理由

ocamlの通信系ライブラリでUnix.open_connectionを使っているライブラリは、Unix.shutdown_connectionを呼ぶだけで切断処理を終わらせているものが多いなあと思いました。

最近さわったredis用のclient libraryとmemcached用のclient libraryはどっちもそういう感じでした。

redis用のライブラリに至っては、サーバーにQUITコマンドを送るだけで終わらせていたりとか。

結果、CLOSE_WAITなsocketが無限に増え続け、いつしかウェブサーバーが起動しなくなったりとかします。

Unix.shutdown_connectionは(名前から想像できるように)内部でsocketのshutdownが呼ばれているわけですが、そもそもshutdownってのはhalf closeと呼ばれていて、「こちら側は終了するつもりですよ」と相手方に教え、相手に先にcloseさせてから、こちらをcloseする前段階として呼び出すものです。

それでこちらのsocket file_descrがcloseされるわけではないので、shutdownだけで終わらせてると、接続するたびに閉じられないfile_descrが増え続けることになります。

で、OSが開くことの出来るファイルハンドルの数には限りがあるので、それが限界を突破した時、ウェブサーバーがエラーログのfileを開くことが出来なくて起動不可な状態になってたり……とかは、割と良くあるパターンです。

じゃあどうやってcloseするかというと、shutodown後に受信用ソケットを0が返るまで読み続け、Unix.close socketするだけ。

でもOcaml標準ライブラリのUnix.open_connectionは、socketのin_channelとout_channelのタプルを返すだけで、socketそのものを返さないので、そうやってsocketを隠蔽しているからには、in_channelを受け取るshutdown_connectionという関数が内部で全てよろしくやってくれるように見えなくもないのですが……

unix.mlの内部を見るとこうなっています。

let shutdown_connection inchan =
  shutdown (descr_of_in_channel inchan) SHUTDOWN_SEND

shutdownと付いているのだから当たり前なのですが、half closeしているだけです。

この後にUnix.closeする処理を付け加えるのは、アプリケーション作成者の責任というわけですね……

書くなら(ブロッキングする処理ですが)こんな感じでしょうか。

let close_connection inchan =
  let buffer = String.create 256 in
  let rec safe_close sock =
    if Unix.read sock buffer 0 256 = 0 then
      Unix.close sock
    else
      safe_close sock in
  safe_close (Unix.descr_of_in_channel inchan)

もちろんshutdownした後に呼ぶ関数です。