NaClの前田です。
松江はそろそろ寒くなって来て、バイクに乗るのがつらい反面、釜揚げそばが美味しい季節になって来ました。
みなさんいかがお過ごしでしょうか。
ちょっと先の話になりますが、12/28の年末ジャンボしまね企業博という何やら景気の良さそうなイベントでブースの留守番をする予定ですので、興味がある方がいたら遊びに来てください(リンク先は企業向けの募集ページです)。
さて、今日はEventMachineを使ったTCPサーバアプリケーションでEM::Connection#get_peername
がnil
を返すという障害報告があったので、ちょっと調べてみました。
EM::Connection#get_peername
はgetpeername(2)というシステムコールのラッパーで、ソケットの接続相手のアドレスを取得します。
getpeername(2)は失敗するとerrno
にエラーの情報をセットしますが、握りつぶしてたんにnil
を返しているようです。素晴らしい。nil
って便利ですね。
get_peername
は同期APIなので例外を返すなりしてくれてもいい気がしますが、イベント駆動入出力と例外は相性が悪いのであえて例外を使わない設計なのかもしれません。
仕方がないのでstraceで調べてもらったところ、getpeername(2)がENOTCONN
で失敗しているようでした。
例えばconnect(2)前のソケットに対するgetpeername(2)がENOTCONN
を返すのはわかるのですが、accept(2)が返したソケットに対するgetpeername(2)がENOTCONNを返すというのはどういう状況だろうと思い、以下のようにクライアントから接続が切断された場合の挙動を確認しました。
サーバ:
クライアント:
しかし、この場合はgetpeername(2)は成功します。
read(2)がECONNRESET
を返すケースもあったとのことだったので、以下のようにRSTで切断するようにクライアント側を修正して再度試してみました。
すると、getpeername(2)がENOTCONN
で失敗しました。
svr.rb:4:in `getpeername': Transport endpoint is not connected - getpeername(2) (Errno::ENOTCONN)
accept(2)が返したソケットに対するgetpeername(2)はRSTで切断された場合にENOTCONN
で失敗することがあることがわかりました。
件のアプリケーションではread(2)などを呼んだ後でgetpeername(2)を呼んでいましたが、accept(2)直後にgetpeername(2)を呼んでおくようにすると失敗するケースが減るのではないかと思います(実際に、上記の例でもsleep(0.1)
を削ると再現しなくなりました)。
ただ、その場合も適宜エラー処理は行うべきだと思います。
EventMachineのAPIについてはもうちょっと何とかした方がいい気もしますが、EventMachineでいいのかというところから悩ましいですね。
このあたりの決定版といえるようなライブラリがないので、TCP接続をたくさん処理するような用途には、現状あまりRubyは向かない気がします(Guildも軽量ではないようですし)。
社内ではNode.jsやGoを使っている案件もあったり、お客さんでErlangを使ってる話も聞きますが、どれもエラー処理はつらそうです。
最近はElixirがよかったりするんでしょうか。
と、だんだん話がずれて来たので、このあたりで筆を置きたいと思います。