IronRuby と MRIの連携(dRuby)編 -連続要求発行不具合調査編-

| コメント(0) | トラックバック(0) このエントリーを含むはてなブックマーク

前回の調査で、連続要求発行の安定動作が見込めないことが分かった。

夜な夜な解析をしたので、そのことについて書いてみる。

 

謎の究明(準備)

ここまで来ると、究明しないといけない。謎の究明開始である。とりあえず、解析準備をはじめる。まずはWindows OS のタスクバーを変更からである。

  1. タスクバーを固定するを解除
    これで、6段分ぐらいの広さにできる
  2. タスクバーを自動的に隠す
    これで、6段分くらいが利用していないときは消える
  3. 同様のタスクバーボタンをグループ化するの解除
    これで、エディタのタスクバーボタンが並ぶ

さらに、エクスプローラ拡張で、フォルダ右クリックで、この配下をgrepできるようにする。これだけ整えば最高である。

参考までに、解析をするときには、基本grepを使う。まず、適当なキーワードで grep し続けていく。grepするとエディタが1つ起動。そこからタグジャンプで別のエディタを駆動する。もちろん、別のエディタで新たな興味が見つかると、その場でgrep。これを繰り返すと、タスクバーに検索順序にあわせてエディタのタスクボタンが並んでいく。しかも、スタック上になるので、何個前のgrep結果とかにも戻りやすい。

謎の究明(現象の確認)

とりあえず、前回実験結果だとログだと、例外が出ること意外、条件がわかっていない。これを究明するためにも、発生状況を把握する必要がある。とりあえず、druby ライブラリのパケットダンプをして、サイズが変わるポイントと、その前後のソケットAPIの戻り値をログとして出力してみることにする。

ワトソン君、そこの LF コードはなんだい?

現象が起きるポイントのダンプを眺めていると、サーバ側の送信はruby の socket api レベルでは正常な送信バイト数が返却される。しかし、受信側では送信バイト数より小さなサイズで返却される。rubyのIO#read( size ) は、指定したサイズを受信するまでブロックする。druby側もこの挙動を利用した設計/実装になっている。しかし、sizeより小さな数が返却されるということは、おかしい。ダンプを眺めていると、16進コード 0x0a (LF) で受信が途切れている。

もしかすると、ソケットのクセにbinmodeで開いていない?かもしれない

名探偵は、常に可能性を列挙していく。もしかすると binmode で開いていない、IronRuby実装系のBUGかもしれない。これを調べるべく、ソースを探索していくと以下のコードを発見する

\ironruby\Merlin\Main\Languages\Ruby\Ruby\Builtins\RubyBufferedStream.cs

public int AppendBytes(MutableString/*!*/ buffer, int count, bool preserveEndOfLines) {
    ContractUtils.RequiresNotNull(buffer, "buffer");
    ContractUtils.Requires(count >= 0, "count");

    if (count == 0) {
        return 0;
    }

    bool readAll = count == Int32.MaxValue;

    buffer.SwitchToBytes();
    int initialBufferSize = buffer.GetByteCount();
    if (preserveEndOfLines) {
        AppendRawBytes(buffer, count);
    } else {
        // allocate 3 more bytes at the end for a backstop and possible LF:
        byte[] bytes = Utils.EmptyBytes;

        int done = initialBufferSize;
        bool eof;
        do {
            AppendRawBytes(buffer, readAll ? 1024 : count);
            int end = buffer.GetByteCount();
            int bytesRead = end - done;
            if (bytesRead == 0) {
                break;
            }

            eof = bytesRead < count;

            buffer.EnsureCapacity(end + 3);
            bytes = buffer.GetByteArray();

            if (bytes[end - 1] == CR && PeekByte(0) == LF) {
                ReadByte();
                bytes[end++] = LF;
            }


このpreserveEndOfLinesが、いわゆる ruby IO#binmode に相当するフラグに違いない。ruby で open() をすると、asciiとして開かれて、改行で苦しめられるのは、先人の体験談である。十分に、ソケットを binmode にしてない可能性が高い。

ワトソン君、君の足の下のクラスはなんだい?

この可能性について、調べていくんだ。ワトソン君。さっそく socket クラスを見つけようじゃないか。あわよくば、コンストラクタに preserveEndOfLines を false にするようにすれば、直るかもしれない。 (このときも、名探偵は、ほかの可能性を否定せず、何かを残しているようだ) ところで、socketクラスはどこにあるんだい?

\ironruby\Merlin\Main\Languages\Ruby\Libraries.LCA_RESTRICTED\socket\TCPSocket.cs

    public class TCPSocket : IPSocket {
        public TCPSocket(RubyContext/*!*/ context, Socket/*!*/ socket)
            : base(context, socket) {
        }

        [RubyMethod("gethostbyname", RubyMethodAttributes.PublicSingleton)]
        public static RubyArray/*!*/ GetHostByName(ConversionStorage/*!*/ stringCast, RubyClass/*!*/ self, object hostNameOrAddress) {
            return GetHostByName(ConvertToHostString(stringCast, hostNameOrAddress), false);
        }

        [RubyConstructor]
        public static TCPSocket/*!*/ CreateTCPSocket(
            ConversionStorage/*!*/ stringCast, 
            ConversionStorage/*!*/ fixnumCast, 
            RubyClass/*!*/ self, 
            [DefaultProtocol, NotNull]MutableString/*!*/ remoteHost, 
            object remotePort) {

            int port = ConvertToPortNum(stringCast, fixnumCast, remotePort);

            Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            socket.Connect(remoteHost.ConvertToString(), port);

            return new TCPSocket(self.Context, socket);
        }


やはり、binmode にしていないではないか。ここで一つ、binmodeにすれば... ん?ワトソン君、君の足元のクラスはなんだい?

\ironruby\Merlin\Main\Languages\Ruby\Libraries.LCA_RESTRICTED\socket\IPSocket.cs

namespace IronRuby.StandardLibrary.Sockets {
    [RubyClass("IPSocket", BuildConfig = "!SILVERLIGHT")]
    public abstract class IPSocket : RubyBasicSocket {


\ironruby\Merlin\Main\Languages\Ruby\Libraries.LCA_RESTRICTED\socket\BasicSocket.cs

namespace IronRuby.StandardLibrary.Sockets {
    [RubyClass("BasicSocket", BuildConfig = "!SILVERLIGHT")]
    public abstract class RubyBasicSocket : RubyIO {
        // TODO: do these escape out of the library?
        private static readonly MutableString BROADCAST_STRING = MutableString.CreateAscii("").Freeze();

        private readonly Socket/*!*/ _socket;

        [MultiRuntimeAware]
        private static readonly object BasicSocketClassKey = new object();

        internal static StrongBox DoNotReverseLookup(RubyContext/*!*/ context) {
            Assert.NotNull(context);

            return (StrongBox)context.GetOrCreateLibraryData(BasicSocketClassKey, () => new StrongBox(false));
        }

        /// 
        /// Create a new RubyBasicSocket from a specified stream and mode
        /// 
        protected RubyBasicSocket(RubyContext/*!*/ context, Socket/*!*/ socket)
            : base(context, new SocketStream(socket), IOMode.ReadWrite | IOMode.PreserveEndOfLines) {
            _socket = socket;
        }



ベースクラスのコンストラクタで、モードを正しく設定しているではないか。アリバイが出てきたぞ。ちょっとまった。でもこのアリバイ、本物か!? TCPSocket#open で接続するけど、その際には本当にこのRubyBasicSocketのコンストラクタが動くのか?もしや、_socket を共有してたりとか、_socketを引き渡して、別のIOオブジェクトが作られるということはないのか?名探偵は、その辺も疑いだす(人間として最低です)

ワトソン君、TCPSocket#open から、このコンストラクタまでどのようにつながってる? *.cs を 'RubyMethod("open' で grep すれば、ほら TCPSocketのなかで...

MiTsuKaRaNaII

この柔らかさはっ!

ワトソン君、みつけたが、この実装はなんだ!。われらが大好き、ラムダ式ではないか。やらかいぞ、やらかいぞ。うふふふふ。

動的言語が好きな名探偵は、ラムダ式を見て狂喜乱舞。ここから、openするオブジェクトのクラスに応じて、どのオブジェクトをnewすべきかやってるみたいだ。この辺は、デバッガでステップ実行しながら確認しないと、さすがにgrepだけではできないぞ。

[RubyMethod("open", RubyMethodAttributes.PublicSingleton)]
public static RuleGenerator/*!*/ Open() {
    return new RuleGenerator((metaBuilder, args, name) => {
        var targetClass = (RubyClass)args.Target;
        targetClass.BuildObjectConstructionNoFlow(metaBuilder, args, name);



疑ってごめんよ。

ラムダ式で、狂喜乱舞しながら、デバッガ片手、grep片手で調べていくと、どうやら、アリバイは本物のようである。つまり、preserveEndOfLinesは真で突き進むのである。となると、容疑者はAppendRawBytesかもしれないし、windowsの精かもしれない。このwindowsの精は、ブラックボックスで、時々いたずらをする。

public int AppendBytes(MutableString/*!*/ buffer, int count, bool preserveEndOfLines) {
    ContractUtils.RequiresNotNull(buffer, "buffer");
    ContractUtils.Requires(count >= 0, "count");

    if (count == 0) {
        return 0;
    }

    bool readAll = count == Int32.MaxValue;

    buffer.SwitchToBytes();
    int initialBufferSize = buffer.GetByteCount();
    if (preserveEndOfLines) {
        AppendRawBytes(buffer, count);
    } else {
        // allocate 3 more bytes at the end for a backstop and possible LF:
        byte[] bytes = Utils.EmptyBytes;

名探偵、30分枠ぎりぎりです。そろそろ犯人を見つけてください

AppendRawBytesをたどっていくと、みつかるのであるが、以下のコード。_socket.Receive(readBuffer, bytesToRead, SocketFlags.None);この部分が、要求サイズ以下でも戻ってくるみたいである。この辺を変更すればOK

\ironruby\Merlin\Main\Languages\Ruby\Libraries.LCA_RESTRICTED\socket\SocketStream.cs

public override int Read(byte[] buffer, int offset, int count) {
    int bytesToRead = _peeked ? count - 1 : count;
    byte[] readBuffer = new byte[bytesToRead];
    long oldPos = _pos;

    if (bytesToRead > 0) {
        int bytesRead = _socket.Receive(readBuffer, bytesToRead, SocketFlags.None);
        _pos += bytesRead;
    }

    if (_peeked) {
        // Put the byte we've already peeked at the beginning of the buffer
        buffer[offset] = _lastByteRead;
        // Put the rest of the data afterwards
        Array.Copy(readBuffer, 0, buffer, offset + 1, count - 1);
        _pos += 1;
        _peeked = false;
    } else {
        Array.Copy(readBuffer, 0, buffer, offset, count);
    }

    int totalBytesRead = (int)(_pos - oldPos);
    if (totalBytesRead > 0) {
        _lastByteRead = buffer[totalBytesRead - 1];
    }

    return totalBytesRead;
}


結果

ちなみに上記を適切に実装すると、前回のテストコードが動作した。

まとめ

まだ、IronRubyは細かいところにbugがあるかもしれない。この後も実用に耐えれるように評価を続けよう

トラックバック(0)

トラックバックURL: http://www.m-tea.info/mt-tb.cgi/17

コメントする

あわせて読みたいブログパーツ

このブログ記事について

このページは、k1ha410が2009年11月16日 19:21に書いたブログ記事です。

ひとつ前のブログ記事は「IronRuby と MRIの連携(dRuby)編 -連続要求発行-」です。

次のブログ記事は「IronRuby と MRIの連携(dRuby)編 -連続要求発行不具合報告編-」です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。