前回の調査で、連続要求発行の安定動作が見込めないことが分かった。
夜な夜な解析をしたので、そのことについて書いてみる。
謎の究明(準備)
ここまで来ると、究明しないといけない。謎の究明開始である。とりあえず、解析準備をはじめる。まずはWindows OS のタスクバーを変更からである。
- タスクバーを固定するを解除
これで、6段分ぐらいの広さにできる - タスクバーを自動的に隠す
これで、6段分くらいが利用していないときは消える - 同様のタスクバーボタンをグループ化するの解除
これで、エディタのタスクバーボタンが並ぶ
さらに、エクスプローラ拡張で、フォルダ右クリックで、この配下を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があるかもしれない。この後も実用に耐えれるように評価を続けよう


コメントする