釜揚げそば

NaClの前田です。

松江はそろそろ寒くなって来て、バイクに乗るのがつらい反面、釜揚げそばが美味しい季節になって来ました。 みなさんいかがお過ごしでしょうか。

ちょっと先の話になりますが、12/28の年末ジャンボしまね企業博という何やら景気の良さそうなイベントでブースの留守番をする予定ですので、興味がある方がいたら遊びに来てください(リンク先は企業向けの募集ページです)。

さて、今日はEventMachineを使ったTCPサーバアプリケーションでEM::Connection#get_peernamenilを返すという障害報告があったので、ちょっと調べてみました。

get_peername

EM::Connection#get_peernameはgetpeername(2)というシステムコールのラッパーで、ソケットの接続相手のアドレスを取得します。

    def get_peername
      EventMachine::get_peername @signature
    end
static VALUE t_get_peername (VALUE self UNUSED, VALUE signature)
{
        char buf[1024];
        socklen_t len = sizeof buf;
        if (evma_get_peername (NUM2BSIG (signature), (struct sockaddr*)buf, &len
)) {
                return rb_str_new (buf, len);
        }

        return Qnil;
}
extern "C" int evma_get_peername (const uintptr_t binding, struct sockaddr *sa, 
socklen_t *len)
{
        ensure_eventmachine("evma_get_peername");
        EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_
t::GetObject (binding));
        if (ed) {
                return ed->GetPeername (sa, len) ? 1 : 0;
        }
        else
                return 0;
}
bool ConnectionDescriptor::GetPeername (struct sockaddr *s, socklen_t *len)
{
        bool ok = false;
        if (s) {
                int gp = getpeername (GetSocket(), s, len);
                if (gp == 0)
                        ok = true;
        }
        return ok;
}

getpeername(2)は失敗するとerrnoにエラーの情報をセットしますが、握りつぶしてたんにnilを返しているようです。素晴らしい。nilって便利ですね。

get_peernameは同期APIなので例外を返すなりしてくれてもいい気がしますが、イベント駆動入出力と例外は相性が悪いのであえて例外を使わない設計なのかもしれません。

仕方がないのでstraceで調べてもらったところ、getpeername(2)がENOTCONNで失敗しているようでした。

切断後にgetpeername(2)を呼んだ場合

例えばconnect(2)前のソケットに対するgetpeername(2)がENOTCONNを返すのはわかるのですが、accept(2)が返したソケットに対するgetpeername(2)がENOTCONNを返すというのはどういう状況だろうと思い、以下のようにクライアントから接続が切断された場合の挙動を確認しました。

サーバ:

require "socket"
Socket.tcp_server_loop(9999) do |sock|
  sleep(0.1)
  p sock.getpeername
end

クライアント:

require "socket"
sock = Socket.tcp("localhost", 9999)
sock.close

しかし、この場合はgetpeername(2)は成功します。

RSTで切断された場合

read(2)がECONNRESETを返すケースもあったとのことだったので、以下のようにRSTで切断するようにクライアント側を修正して再度試してみました。

require "socket"
sock = Socket.tcp("localhost", 9999)
sock.setsockopt(Socket::Option.linger(1, 0))
sock.close

すると、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がよかったりするんでしょうか。

と、だんだん話がずれて来たので、このあたりで筆を置きたいと思います。