2007年09月03日

このサイトについて

このサイトは、「dRubyによる分散・Webプログラミング」という書籍の一読者であるえがぴ~が、自分自身のdRubyに関する理解を深めるために記述したメモです。不定期に書いていきます。

・目次


・このサイトの説明

  1. 目的とすること
    • dRuby本での解説文や掲載されているサンプルのソースコードを理解する(えがぴ~がわからない、人に聞かれて説明できない箇所を無くす)※ただし、3章のeRubyと6章のWebアプリケーションは除外する予定です。
    • dRubyや関連プロダクトの使い方を理解する
    • Rubyもあまりよくわかっていないので、ついでにRubyの構文も理解する

  2. 目的としないこと
    • dRubyの内部構造を把握する
    • 多分私の力では無理だからです。ただ、疑問に思った点は追いかける努力だけはすることにします。

      2007.9.10 追記:やってるうちに趣がだいぶ変わってきました。中身はさらっと追いかけています。

    • 分散オブジェクトシステムそのものについて解説する
    • これも多分私の力では無理だからです。ただ、思ったことや疑問は書くことにします。

  3. えがぴ~の通信簿(雰囲気)
    • C/C++:3
    • Java:4
    • Ruby:1

  4. 読んで頂いた奇特な方へ
  5. お気軽にコメント下さい。誤りの記述など、ツッコミ歓迎です。



2007年09月05日

はじめに

では、「はじめに」から読み始めます。さてさてdRubyとはどんな感じのものなのでしょうか。

dRubyは、ごく普通のRubyオブジェクトを、プロセス/ネットワークを越えてやりとりしたり、呼び出したりするためのシームレスな仕掛けです。

はじめに

また、「はじめに」の後半で、dRubyは、

普通のRubyのオブジェクトがプロセスの壁を越えて話し合う・・・

はじめに

とあります。dRubyとはどうやらRubyのオブジェクトを、どこか外にある別のものと相互作用できるようにするものだとわかります。ここで言う「プロセス」とは、UNIX系だとpsや、Windowsだとタスクマネージャーのプロセス欄に表示されるものです。(プロセスと言う用語は様々な意味で用いられるため、わかりにくい用語のうちの一つですね。。プロセスを、本サイト中ではこの意味でしか使いません。)

プロセスは、生成されるときにOSから各々別の資源(プロセス空間)が割り当てられるため、マルチプロセスOSでは、原則はどんなに馬鹿で粗相をするプロセスがあっても、それ以外のプロセスに影響を及ぼさないように出来ています。であるがゆえに、プロセス間で情報の受け渡しなど相互にやり取りをするためには、それ用の特別な仕組みを利用する(プロセス間通信)必要があります。プロセス間通信の方法はOSによって様々です。

そういえば元々UNIXでの制御系エンジニアだったので、セマフォとか、ソケットとか、共有メモリとかを使ったプロセス間通信を結構やってたなあ。dRubyはそういったプロセス間で通信する仕組みなんだな。という程度の解釈でとりあえず次に進みます。

dRubyの背後に見るものはなんでしょう。

はじめに


なんだろ。楽しみです。が、まだ背後は早い、ということで、反義語の「正面」と言う名前をこのメモサイトにつけることにします。

1章.分散オブジェクトシステムdRuby

本編「1章.分散オブジェクトシステムdRuby」に入ります。ふむふむ、dRubyは“分散オブジェクトシステム”というものであると。

  1. 今までになかったのか
  2. では分散オブジェクトシステムって何だ、今までになかったのか、と言う疑問がわきますが、それは「1.2 RPCとRMI」付近に書かれています。図があってわかりやすいです。なるほど、RPCは外部へ呼び出しを委譲するだけで状態を持たないということかしら。それがRMIとかだと、ライフサイクルの長いオブジェクトが扱えるということですね。で、分散オブジェクトシステムとはRMIのようなものだということか。ということで「1.4」「1.5」あたりへ読み進める。

  3. dRubyはどうすごいのか
    • 対象をRubyに限定している
    • 100%Rubyで書かれている
    • IDL不要

    1.5 dRubyの特徴

    プロセス間のやり取りに、OSに依存する仕組みを使わずRubyで実装しているのでRubyが動く環境であればどこでも動くと言うことがすごいのかな。で、IDL不要と。

    Rubyの変数には型がなく、継承関係を利用した代入原則がありません

    1.5.2 Rubyの特徴そのままに

    対偶ぽくすると、“JavaやC++の変数には型があり、継承関係を利用した代入原則があります”とも言いかえられそう。「継承関係を利用した代入原則」とは、変なもの{を|に}入れるとClassCast例外が出るアレのことですねおそらく。変数に型があると、任意のものを何でも入れられるわけではないし、型を知らないものは使えない。と言うことだと思いました。対して、変数に型がない場合は何でも使えるようになるのでIDLも要らなくなるということですね。で、dRubyはそっち系だということか。

    そういえば、Java RMIで、とりあえずやってみる手順としては


    1. どこかにRMIレジストリサーバーを立てる(rmiregistry)

    2. ポリシーファイルを作る(java.policy)

    3. リモートオブジェクトを提供するプロセスやリモートオブジェクトを使うプロセスを動かす

    となりますが、それほど面倒ではないと思います。私はこの手順くらいなら許容できます。しかし、RMIでは共有しようとするオブジェクト(のクラス)に対して以下のような制限があります

    • 共有するオブジェクト(のクラス)は、java.rmi.Remoteインタフェースを継承したインタフェースをインプリしていること

    • 共有するオブジェクト(のクラス)のメソッドにjava.rmi.RemoteExceptionというチェック例外を定義しておくこと

    • 共有するオブジェクト(のクラス)のスタブ(など)を予め作成しておくこと(rmic)※この手順はJava5以上だと動的プロキシを使うため要らないっぽい

    これは面倒な気がします。転じて、dRubyの場合、


    1. リモートオブジェクトを提供するプロセスやリモートオブジェクトを使うプロセスを動かす


    という手順のみでいけるということか。えらく楽ですね。リモートオブジェクトの場を提供するプロセスが、扱うオブジェクトの型を知らなくてもいいと言う点が楽にしているのかな、と言う理解で1章は終了。

    「1.5.3 参照渡し/値渡し」は、もうちょっと後のページで解説されるみたいなので、ここではさらっと読むだけにします。

    次は2章、いよいよコーディング。


2章.Hello, dRuby (その1)

「2章.Hello, dRuby」に入りました。まずは「Hello, World.」ですね。

この実験では二つのプロセスが登場します。一つは文字列を印字するサーバ、もう一つは印字サーバを利用して「Hello, World.」を putsするクライアントです。

2.1 Hello, World.

まずはサーバー側となるputs00.rbの方からソースコード(リスト2.1)を、順に見ていくことにします。私はRubyをちゃんと勉強していないので、ついでにRubyのことも詳しめに見てみることにしてみようと思います。

require 'drb/drb'

Cでいうincludeみたいなもののようです。使いたいモジュール(モジュールと言う呼称でいいのかはわからないが)をこれで含めると、インタプリタがそっちを先に読みに行って、そこに定義されているものが使えるようになる。と言う感じに理解しておきます。

class Puts

classでクラスを定義できます。

def initialize(stream=$stdout)

defはメソッドを定義できるようです。で、initializeって名前のメソッドはコンストラクタのようです。
$で始まるものはグローバルな変数です。なので$stdoutはグローバル変数のうちのひとつです。標準出力を指し示す組み込み変数(あらかじめ定義されている変数)のようです。

@stream = stream

Rubyでは、@がついてるやつはインスタンス変数になります。ここでは引数のstreamをインスタンス変数の@streamに代入してるってことですね。

uri = ARGV.shift

 うごご、わからん。でもなんとなく想像はつく。調べてみると、ARGVはどうやらコマンドライン引数が入ってくる配列のようです。(Cのように、0番目にプログラム名が入ってきたりはしません。)で、shiftは、先頭の一要素を取るメソッドのようです。shiftが実行されると、先頭の要素は取り除かれ、他の要素はひとつずつ詰められるようです。ほうほう。puts00は、実行時にコマンドライン引数の先頭にURIを指定してもらう仕様であるということですね。

DRb.start_service(uri, Puts.new)

ついにきましたDRb。解説文を見てみると

3. サービスの場所を示す名前であるURIと、URIに関連付けるオブジェクトを指定して dRubyのサービスを開始します。これによってリクエストを待機するスレッドも準備されます。URIに関しては後述します。

2.1.1 印字サーバ

とあります。にゃるほど。「リクエストを待機するスレッドも準備されます。」と言うところが重要そうだな思いつつ次のステートメントを見てみます。

sleep

 これはスレッドを止めるやつのようです。むむむ、止まっちゃうんだよね。で、このプログラムはsleepが最後の行だからもし復帰したら終わっちゃうんだよね。

 ちなみにスレッドとは、処理を実行する単位とか、処理の実行者を指したり、処理の流れそのものを指したりします。プロセス内の処理を実際に実行しているのはスレッドになります。どこかから起動されたRubyのプロセスは必ずひとつ以上のスレッドを持ちます。最初に立ち上げられるのはメインスレッドと言うみたいです。同プロセス内のスレッドは、プロセス空間(メモリの場とか)を共有します。Rubyでのマルチスレッドの話はまた後で出てくるみたいなので楽しみにしておきます。

 さて、ここのsleepは「スクリプトが終了してしまわないように、メインスレッドをsleepさせます。」とあります。

 そもそもsleepってどこのメソッドなのかなあ。Kernelモジュールのsleepってやつが呼ばれてるのかしら。。KernelモジュールはWindowsだと多分kernel32.dllをロードして使えるようにしてるのかなあ?...っと予想通りwin32.cでやっていました。windows.hをincludeしてるWin32SDKプログラミングですね。濃いのでRuby本体のソースはこれ以上追いかけないようにして。。。。組み込み関数のsleepの説明を見てみると、

sec 秒だけプログラムの実行を停止します。sec には浮動小数点数も指定できます。sec が省略された場合、他スレッドからの Thread#run などで明示的に起こさない限り永久にスリープします。戻り値は実際に停止していた秒数(整数)です。

とあります。で、どこからでも呼べると。呼んだら、それを実行していたスレッドが止まると。ここでは引数を指定していないので、起こされない限り永遠に止まっていると言うことですね。で、もし起こされてもこのプログラムはsleepが最後の行だからそのまま終わると。ふむふむ。

 じゃあ、やっぱりDRb.start_serviceで新しいスレッドを起こしていないと何も出来ないはずです。ちょっとだけみてみるかな、難しかったらあきらめよう。。と、start_serviceメソッドの中では、DRbServerをnewしてるな、でDRbServerのinitializeの中で

@thread = run

としている。これかなあ。runをみてみると

def run
  Thread.start do
begin
  while true
    main_loop
  end
ensure
  @protocol.close if @protocol
  kill_sub_thread
end
  end
end

うごご、tabと半角スペースが雑じっててなんだかインデントがわからぬデス。。。っと、それはさて置き、RubyのThreadはstartメソッドにブロックを渡せるんですね。runメソッドではThread.startで新スレッドを生成、実行し、そのオブジェクトが勝手に返却されてると。で、そのスレッドの中のブロックはぐるぐる回ってるmain_loopがいる感じですね。ほうほう。とりあえず要求を受け付けるスレッドは作られてそうだということが確認できたので、puts00については満足。

まだサーバー側しかみていませんが、ソースコードが入ると長くなる予感。この調子でどこまで行けるか。。と一抹の不安を覚えつつ続きはまた後日。

2007年09月06日

2章.Hello, dRuby (その2)

「Hello, World.」の続きです。印字サーバー側(puts00)は終わったので、本の指示に従い起動したままほったらかしておきます。次はクライアント側、本の「2.1.2 irbからの利用」あたりからです。クライアント側はirbでやるんですね。irb(interactive ruby)は、Rubyと逐次対話しながら遊べる道具です。

there = DRbObject.new_with_uri('druby://localhost:12345')

と、irbでタイプします。

DRbObject.new_with_uri()は、URIを指定して分散オブジェクトへの参照を返します。 URIはputs00.rbを起動したときに与えたものを指定します。生成された参照(DRbObject)をthereという変数に覚えておきます。

2.1.2 irbからの利用

ふむふむ、ちゃんと理解するためにDRbObject.new_with_uriの中を見てみると、

self.new(nil, uri)

してるだけのようです。selfってのは自分を指すのでしょう。が、selfの厳密な意味や仕組みを深く突っ込んでいくとハマりそうな気配がするのでちょっとうやむやにしておます。ここでは、DRbObjectのインスタンスが生成されている、という程度の理解で留めます。つまり、どんなオブジェクトがサーバーで共有されていようが、リモートオブジェクトの参照は、必ずDRbObjectだということですね。クライアント側からリモートオブジェクトにアクセスするには必ずDRbObjectを介するので、DRbObjectさえ理解すりゃアクセスの仕組みはわかると。そういうことと理解しておきます。

じゃあ、次にみるべきところはDRbObjectのinitializeかなあ。中を見ると、、、まず、第1引数のobjがnilかどうかで分岐しています。でもどちらも@uriと@refに設定していることに違いは無いようです。で、この場合は必ずnilなので、そっちのブロックを見ることにします。

@uri,option=DRbProtocol.uri_option(uri,DRb.config)

うごご、「@uri,option = ・・・」なんじゃこりゃ、全然わからん。一体@uriには何がセットされるんだ、、、で、30分以上悩みました。。が、これは多重代入と言うんですね。右オペランドと左オペランドの要素数が複数個でも入れれるだけ入れてくれると。ふむふむ。例えば

foo, bar = 1,2;p foo;p bar

とすると、

1
2

と出ます。実際には[1,2]と配列になって返却されているようですね。こんなことも知らないとは、、と言われそうですが知りませんでした。Ruby(というかスクリプト言語)の通信簿1ですからね。はい。またちょっと賢くなりました、さて、ここでは、DRbProtocol.uri_optionを呼び出し、uriとoptionを返してもらっています。引数になっているDRb.configあたりを追いかけると出てくるcurrent_serverは、いたるところで使われていてかなり重要そうな気がしますが、とりあえずこれは今回は置いておきます。なので、次は素直にDRbProtocol.uri_optionを見ていきます。(多分元のuri文字列を加工してるんだろうなという想像をしつつ。)

    def uri_option(uri, config, first=true)
      @protocol.each do |prot|
	begin
	  uri, opt = prot.uri_option(uri, config)
	  # opt = nil if opt == ''
	  return uri, opt
・・・

まず、@protocolは配列なので複数のプロトコルをサポートしているようです。デフォルトではDRbTCPSocketの一要素のみが設定されています。で、これが「druby://ホスト:ポート番号」のやつだと。でも、正確には「druby://ホスト:ポート番号?オプション」と言う形式で指定される仕様のようです。@protocolのuri_optionメソッドはDRbTCPSocketの同名メソッドに委譲しています。この中で指定されたURI文字列をパースして、ホストやポート番号、オプションに分解しているようですね(多分同じことはDRbServerの方でもやってるんでしょうが、main_loopの中まで見ていなかったので遭遇しなかったようです)。

@uri周りはこれ位で満足しました。次は@refです。もう一度DRbObjectのinitializeに戻って

@ref = DRbURIOption.new(option) unless option.nil?

これは、unlessだから、「optionがnullでなければoptionを@refに代入しろ、optionがnullならDRbURIOption.newして@refに代入しろ」ってことです。理由はわかんないけどオプションを単なる文字列じゃなくてDRbURIOptionオブジェクトとして持っておきたいってことなのだろうという程度の理解で次。
ってあれれーー??もう終わった。設定のみか。何か簡単すぎるような気もするが気にせず進めていきます。これで、irbでさっきタイプした

there = DRbObject.new_with_uri('druby://localhost:12345')

のthereに、uriやoptionが設定されたDRbObjectのインスタンスが代入されるんだよ、ってところまでは理解しました。で、引き続き次はirbで、

there.puts('Hello, World.')

と打つとサーバー側のputs00を動かしているターミナルに表示される。うん、すごい!!でもどうやってんだろ、DRbObjectには当然ながらputsなんてメソッドはありません。でも呼べてるんだから何かある。。と、これはどうやらmethod_missingメソッドの中でやっています。RubyのObjectクラスのリファレンスを見ると

method_missing(name, args, ... )

呼びだされたメソッドが定義されていなかった時、Ruby がこのメソッドを呼び出します。

とあります。DRbObjectではこのメソッドをオーバーライドして、その中でリモートのメソッド呼び出しをやっているんですね。この中身も見たいところだが、今日はもう時間切れ。とりあえず「Hello, World.」が出るのは出た!

後、クライアントからputsで文字列を渡すとサーバー側の標準出力に出るってのは確認できましたが、サーバー側のリモートオブジェクトからクライアント側の@stdoutには出せないのかとか、色々気になりはじめたことがあります。が、これらも今追求せずとも読み進めるにつれて明らかになっていくでしょう。

2007年09月07日

2章.Hello, dRuby (その3)

「2章.Hello, dRuby の3回目(その3)」ですが、前回のエントリでは見れなかったDRbObjectのmethod_missingの中が気になるので、今回は本を読み進めず、こちらの中の概要だけでも見てみることにしました。以下がDRbObjectのmethod_missingの全てです。

def method_missing(msg_id, *a, &b)
  if DRb.here?(@uri)
obj = DRb.to_obj(@ref)
DRb.current_server.check_insecure_method(obj, msg_id)
return obj.__send__(msg_id, *a, &b) 
  end
  succ, result = self.class.with_friend(@uri) do
    DRbConn.open(@uri) do |conn|
      conn.send_message(self, msg_id, a, b)
    end
  end
  if succ
    return result
  elsif DRbUnknown === result
    raise result
  else
    bt = self.class.prepare_backtrace(@uri, result)
result.set_backtrace(bt + caller)
    raise result
  end
end

すごくちょっとしかありません。これくらいならみれるかなあ。じゃあまず最初の分岐。

if DRb.here?(@uri)

これは、スクリプトを実行している自分がオブジェクトを公開しているサーバー自身かどうかを訊ねています。自分がオブジェクトを公開しているサーバーだったら、リモート呼び出しの仕組みを使わずにそのまま呼んじゃえという事だと思われます。対して、リモート呼び出しのときに実行されるのが以下。

succ, result = self.class.with_friend(@uri) do
  DRbConn.open(@uri) do |conn|
    conn.send_message(self, msg_id, a, b)
  end
end

かなり短いですが、結構ムズい。渡されるブロックが入れ子になって2つあります。多分yieldの2段呼びだと想像しつつ、この5行をもうしばらく眺める。ちなみにyieldとはメソッドにブロックを渡すとそれを任意のタイミングでメソッドの中から呼べるというものみたいです。こんなやつです。

def add 
  p '足します'
  yield(1,2) 
  p '足しました'
end
add do |a,b|
  p a + b
end

上のスクリプトを実行すると、下のように出力されます。

足します
3
足しました

 大抵の場合、yieldは上の例のようなヘボい使い方はせず、イテレータで回す時に使われるようです。yieldの説明はこれくらいにして、さっきの5行ですが、よく見ると単純で、多分サーバーへの接続を確保し、そこへメソッド呼び出しのメッセージを送るという処理なのだなと言う想像くらいは容易につきます。これをわざわざyieldでブロックを渡してやろうとしているということは、複数の接続先が配列などで管理されていて、そこから今回の送り先を選び出して送るか、または、送り先全てに対して順次送ったりするんだなと推測出来そう。っと、そういう前提を抱きつつ中を見てみることにします。

def self.with_friend(uri)
  friend = DRb.fetch_server(uri)
  return yield() unless friend
・・・

 。。。。はい、全然よくわかりません。。friendって何かしら。お友達関係にあるサーバーと連携できるのかな。とりあえずHello, World.レベルの単純なクライアントからの場合はfriendはnilっぽいですね。とりあえず今回はfriendはnilと読みかえることにします。だからこのreturn yieldも実行されて、中のDRbConn.open(@uri) do ・・・ブロックが動きます。

 DRbConnとは、DRbObjectの中で使われる単純なクラスで、具体的にはサーバーとの接続をプールしているみたいです。毎回繋げに行くのはコストが掛かるから、一度つなげた接続は使いまわして、メソッド呼び出しに掛かるコストを減らしてくれるんですね。で、えっと、DRbConn.openなので、これはクラスメソッドか。Rubyでは“self.メソッド名”と定義すると、クラスメソッドになるようです。なので、self.openメソッドを見てみます。

def self.open(remote_uri)  # :nodoc:
  begin
conn = nil
・・・
conn = self.new(remote_uri) unless conn
succ, result = yield(conn)
return succ, result
・・・

 実際のコードはpoolにいれたりpoolに照会したり、poolに対するアクセスをアトミックにしたりともっと長いけど、肝心なところだけ抜粋したつもりです。poolに何も入っていない最初のアクセスの場合は、connもnilのままなので、ここでDRbConnのインスタンスを作って、それ自身を渡してyieldですね。で、そうすると2段ブロックの中の方が動いてその結果を返すと。ふむふむ。ということで次はDRbConnのsend_messageです。

def send_message(ref, msg_id, arg, block)  # :nodoc:
  @protocol.send_request(ref, msg_id, arg, block)
  @protocol.recv_reply
end

ここでのrefはDRbObjectですね。msg_id, arg, blockは、method_missingに渡された引数そのままです。@protocolはDRbTCPSocketだということは以前のエントリで既に見たので、DRbTCPSocketのsend_requestを見てみます。

def send_request(ref, msg_id, arg, b)
  @msg.send_request(stream, ref, msg_id, arg, b)
end

ちなみに@msgは何物かというと

@msg = DRbMessage.new(config)

 と初期化されていました。他プロセスのオブジェクトに対して実際にメッセージを送るのはDRbMessageがやっているようです。で応答をrecv_replyで受け取ってsuccとresultに詰めるという感じです。succが成功or失敗の真偽値、resultがメソッドの戻り値です。ちなみにDRbMessageの中ではMarshalを使ってdumpしているようです。詳しくはまた今度見ることにします。

 終わった。送信部分の流れは大体わかりました。もしかして後はDRbServerのmain_loopの中さえ見りゃ、dRubyの基本的な内部構造にはさらっと目を通したことになる。か?

 ところで、yield2段を目で追うのがつらかったので、今回からデバッガを使い始めました。しかし、そこで少しハマりました。デバッガとかで見ていると、デバッガが変数のインスペクション情報を取得するために勝手にto_sとかを呼び、私自身が試そうとしているスクリプトよりも先にリモート呼び出しをしているので、既にDRbConnにプールされていました。くそー、まだ一度もリモートで呼び出ししていないからインスタンス出来てないはずなのにどこで作ってるんだよ、わからん、って思って悩んだというわけです。それは置いといて、デバッガもリモートオブジェクトだと言う意識がないということだなあ。デバッガも騙されるdRubyって。。ちょっとびっくりです。

 次から本に戻ります。どこを読んでたかなあ。あ、まだHello, World.か。

2007年09月10日

2章.Hello, dRuby (その4)

本の続きに戻ります。サーバを停止させた状態でリモート呼び出しをしたらどうなるか実験してみます。puts00のサーバーを停止してthere.putsを実行っと。

DRb::DRbConnErrorという例外が発生しました。 DRbConnErrorはdRuby内部で通信エラーが発生したことを示します。サーバが停止してしまったので、メソッド呼出しに失敗してしまったのです。

2.1.2. irbからの利用

確かに本の通りにDRbConnErrorがでました。

def initialize(remote_uri)   # :nodoc:
  @uri = remote_uri
  @protocol = DRbProtocol.open(remote_uri, DRb.config)
end

場所はここ、DRbConnのインスタンス生成時のDRbProtocol.open。このメソッドは

def open(uri, config, first=true) 
  @protocol.each do |prot|
    begin
      return prot.open(uri, config)
    rescue DRbBadScheme
    rescue DRbConnError
      raise($!)
    rescue
      raise(DRbConnError, "#{uri} - #{$!.inspect}")
    end
  end
  ・・・

 です。このprot.openでは、実際にはDRbTCPSocketのクラスメソッドopenが呼ばれるので、この中のTCPSocket.openで例外が発生している感じですね。それをrescueでつかまえてraiseしてるのか。ふむふむ。ではopenは成功したけどその後に通信エラーになった場合はどうなるのかなあ。DbConnのopenの中身の一部です。

  ・・・
@pool.each do |c|
   if conn.nil? and c.uri == remote_uri
     conn = c if c.alive?
   else
     new_pool.push c
   end
end
@pool = new_pool
  ・・・

 プールから取る時の、「conn = c if c.alive?」で、この時点でサーバー側が死んでいたらそれを使わないようになっています。で、もしここでも接続は生きてたけど、送信までに死んだ場合は、recv_replyのどこかで例外が出るようです。
 ここでは、@poolの要素群をnew_poolという入れ物に一度移し替えて、それを@poolに戻しています。ただ今回使われることになった接続オブジェクトは、この時点ではnew_poolに入っていません。プールに接続オブジェクトを入れているのは、その下に記載されているのensureです。(ensureってのは、javaでいうfinallyみたいなやつです。)

@pool.unshift(conn)
@pool.pop.close while @pool.size > POOL_SIZE

 新しいconnをプール用配列の先頭に入れ、プールの要素数の上限を越えた要素は取り除いて閉じる。ってことか。whileも右側に書けるのですね。かっこいいコードだなあ。

 
 ところで、サーバーは無事生きているけど、リモートオブジェクトに存在しない操作を行なった場合はどうなるのでしょうか。ここではhogeメソッド(Putsに存在しない操作)を呼んでみました。

there.hoge

すると以下のようなresultになりました。ちなみに、成功/失敗が設定されるsuccの値はfalseになっています。

result undefined method `hoge' for #<Puts:0x29ba790 @stream=#<IO:0x294e758>>

2007年09月11日

[番外]今までの整理

ひとまず今までのところで学んだことを整理することにしました。

dRubyとは

  1. Rubyのオブジェクトをプロセス間でやり取りするための仕掛け
  2. dRubyは100%Rubyなので、変数が型に縛られないから楽に使える。IDLとかもいらない
  3. dRubyはプロセス間通信の仕組み自体もRubyで書かれているので、OSに依存しない

dRubyの仕組み

  1. URIに対してひとつのオブジェクトを紐付けられる
  2. 複数のプロトコルを扱える。デフォルトはDRbTCPSocket。自分で追加も可能
  3. URI文字列のパースや、メッセージをやり取りするときにProtocol関係のオブジェクトへ委譲されるため

  4. サーバー側でどんなオブジェクトを公開してようとも、クライアントが扱えるリモートオブジェクトは常にDRbObject
  5. DRbObjectを使った呼び出しであっても、自分自身がサーバー側であれば、ローカルのオブジェクトのメソッドを直接呼ぶ
  6. DRbObjectのmethod_missingからリモート呼び出しをしている
  7. メッセージの送受信を実際に行っているのはDRbMessage
  8. サーバーへの接続はDRbConnによってプールされ、接続の確立にかかるコストを抑えている

まだわからないところ

  1. puts00でサーバー側のPutsからクライアント側の@stdoutに出すにはどうしたらいいの?
  2. サーバー側は、マルチスレッドで並列にメッセージを処理している?
  3. まだmain_loop内を見ていないのでわからない
  4. fetch_serverのfriendって何?どういうときに使われるの?
  5. 通信時のMarshalを使った仕組みの詳細
  6. プールのところにある#FIXMEがなぜFIXMEなのか
  7. URIのoption引数の使い方、というか使われ方

2007年09月12日

[番外]単純な送信時のメッセージパッシングの雰囲気

今まで学んだ部分(クライアント側)をコミュニケーション図で整理してみました。が、正確ではありません。yieldや、moduleの扱いをどう描いたものか難しいですねえ。。クラスメソッドとインスタンスメソッドも区別していません。細かいところも結構端折ってます。なので、あくまでも雰囲気ということで。

(画像をクリックすると別窓に表示します)

2007年09月14日

2章.Hello, dRuby (その5)

本の続きに戻ります。「2.1.4 dRubyのURI」からです。小見出しの通り、DRbServerとURIとの関係についてのお話です。

TCPを使って接続する場合のdRubyのURIの書式は次の通りです。

druby://ホスト名:ポート番号

URIはホスト名とポート番号から構成されます。 URIによってdRubyのサーバを特定することができます。

URIを使用するのは、サービスの起動時とDRbObjectの生成の場面です。 DRb.start_serviceはURIとそれに関連付けるオブジェクトを指定して dRubyのサービス(DRbServer)を開始します。 DRbServerにはサーバソケットの管理やソケットを待ち受けるスレッドなどサーバプログラミングに必要なもの一式が入っています。具体的には、URIのホスト名とポート番号を用いて生成したTCPServerや、 acceptで接続を待ち受けるスレッドなどのオブジェクトです。 URIはDRbServerを特定する情報で、DRbServerは一つの固有なURIを持ちます。

DRb.start_service(uri, front)

2.1.4 dRubyのURI

ふむふむです。今までのエントリで、さらっと中身を覗いたこともあり、大体感じはわかった。ちなみに、本文中のacceptとは、クライアントからの接続があるまで待ち続けるシステムコール(もしくはシステムコールを呼び出すことのできる何か)のことだと思います。ソケット通信とかのサーバーのプログラムを自分で書くときは、大抵の場合、acceptで待ち続け、クライアントからの要求に応じてスレッドを生成したり、forkして子プロセスを作ったりすることが多いと思います(forkとは分身の術のようなものです。)。こうすることでクライアントからの複数の要求を並列に処理することが出来ます。dRubyのDRbServerのmain_loopでも、スレッドを生成して並列に処理しているのだと思います(と想像)。で次と。

URIと関連付けられたオブジェクトは、そのサービスの入り口(受け付け担当)となるため、フロントオブジェクトと呼びます。 DRbObject.new_with_uri()で作成したオブジェクトへのメソッド呼び出しは、全てこのフロントオブジェクトに届くことになります。実際のアプリケーションでは、アプリケーション内部のオブジェクトをそのまま URIに関連付けずに、アクセス制御をしたり、複数の処理をまとめて行うユーティリティ的なメソッドを用意したりした、特別なクラスをフロントに置くことが多いです。

2.1.4 dRubyのURI

DRbServerで公開するリモートオブジェクトは、フロントオブジェクトと言うのですね。で、門番とか総合受付みたいなことをさせることが多いと。図がわかりやすいです。オブジェクトを公開しているDRbServerはURIによって特定されると言うことなので、URIの理解が大事なようです。本ではしばらくURIの話が続きます。URIは下記のように一部省略できるようです。

'druby://hostname:12345' - 全て指定した完全な形
'druby://:12345' - ホスト名を省略した形式
'druby://hostname:0' - ホスト名を指定するがポート番号を省略した形式
'druby://:0' - ホスト名もポート番号も省略した形式
nil - ホスト名もポート番号も省略

2.1.4 dRubyのURI

続いて、本に従いirbで実行っと。。ちゃんとできました!!別のコンピュータとも問題なくやり取り出来ますね、dRubyのオブジェクトのやり取りは、標準でTCPSocketを使っているため、同じコンピュータのプロセス間のみではなく、別コンピュータのプロセスともやり取りできます。可能性無限大。

。。。でも、あれれ?先日のエントリで確認した限りでは、「druby://」のプロトコルでは、ポート番号の後に引数を追加して何か出来るような気がしました。

druby://ホスト名:ポート番号?オプション

URIのパース部分の正規表現はこう


/^druby:\/\/(.*?):(\d+)(\?(.*))?$/

オプションは確かDRbURIOptionになっていたやつでした。これは何に使うんだろうなあ、という疑問がわきます。メッセージ送信部分のソース(DRbMessageのsend_request)を一応みてみると

def send_request(stream, ref, msg_id, arg, b) # :nodoc:
  ary = []
  ary.push(dump(ref.__drbref))
  ary.push(dump(msg_id.id2name))
  ary.push(dump(arg.length))
  arg.each do |e|
    ary.push(dump(e))
  end
  ary.push(dump(b))
  stream.write(ary.join(''))
rescue
  raise(DRbConnError, $!.message, $!.backtrace)
end

とあります。ここのref.__drbrefは、オプション引数DRbURIOptionそのものなので、dumpされた情報には確かに含まれているようです。おそらくサーバー側でのマーシャルデータ復元時に、DRbObjectのrecv_requestとかそのあたりでよきに計らってくれるようですが、ちゃんと追いかけることができていません。このあたりの世界は、もうちょっと本を読み進めてから再挑戦することにします。

でも、内部の仕組みはともかく、オプションの利用方法くらいは知りたいなあ。と思って調べたら、咳さんのはてなに載ってました。

URIから参照を作ろうとすると、サービスの入り口のオブジェクトしか参照できないことになるんだけど、数年前に追加されたオプションを使うともうちょっと先のオブジェクトを参照できる。

druby://localhost:12345?option

「?」の後の文字列はURIのオプション引数(?)となり、frontオブジェクトにこの文字列を問い合わせた結果のオブジェクトを参照する。問い合わせはメソッドに?以後の文字列を与えて行う。

I like Ruby too. - dRubyのURIのオプション

ふむふむ、そういうことか。(てか、最近のエントリでこのサイトも紹介してくれてる!)

雰囲気を味わうために、puts00.rbをちょっと改造してみます。

require 'drb/drb'

class Puts
 def initialize(stream=$stdout)
  @stream = stream
 end
 def puts(str)
  @stream.puts("#{str} [Puts]")
 end
end

class Puts2
 def initialize(stream=$stdout)
  @stream = stream
 end
 def puts(str)
  @stream.puts("#{str} [Puts2]")
 end
end

class Front
 def initialize
  @hash = {"puts" => Puts.new, "puts2" => Puts2.new }
 end
 def [](arg)
  @hash[arg]
 end
end

uri = ARGV.shift
DRb.start_service(uri, Front.new)
puts DRb.uri
sleep

Puts2という、Putsのようなものをもうひとつ定義し、それぞれのインスタンスをハッシュに入れて切り替えられるようにしただけです。
ちなみに{}で定義できるものをハッシュといい、キーと値のペアで情報を出し入れできる便利ないれものです。なお#{hoge}は式展開といい、文字列中にオブジェクトの内容を入れることができる書き方です。Ruby好きな人は式展開が好きらしいです。

はい、で、ハッシュのキーをオプションの文字列にしました。で、クライアント側のコードは

require 'drb/drb'

there = DRbObject.new_with_uri('druby://localhost:12345?puts')
there.puts('Hello, World.')

there2 = DRbObject.new_with_uri('druby://localhost:12345?puts2')
there2.puts('Hello, World.')

とかで。するとこんな風に出ます。一応出来たのは出来た。

Hello, World. [Puts]
Hello, World. [Puts2]


今回はこれにて終了。次はリマインダーのサンプルを見ていきます。

2007年09月20日

2章.Hello, dRuby (その6)

今日は、dRuby幸福の王子本のp.16 「2.2 Reminder」からです。ここでは、Reminderのアプリケーションを作って動かしてみます。いよいよdRubyを使ってアプリケーションを作成するときが来ました。要件は本に記載されている通りです。

だれでも書き込め、削除できるメモ帳。
ユーザインターフェイスはirb。

2.2 Reminder

本の図を見ると大体の構造がわかりますね。公開されるReminderのオブジェクト(フロントオブジェクト)は以下です(転載)

# reminder0.rb
class Reminder
 def initialize
  @item = {}
  @serial = 0
 end

 def [](key)
  @item[key]
 end

 def add(str)
  @serial += 1
  @item[@serial] = str
  @serial
 end

 def delete(key)
  @item.delete(key)
 end

 def to_a
  @item.keys.sort.collect do |k|
   [k, @item[k]]
  end
 end
end

メモの文字列はハッシュに入れられると、で、ハッシュには1からのシリアル番号が振られ、それがキーになり添字でアクセスできるようになっていると。はい、理解しました。で、本に従い、irbからサーバーとクライアントを立ち上げて、リマインダーに登録してみます。

r.add('13:00 ミーティング')
r.to_a
=> [[1, "13:00 ミーティング"]]

無事登録できましたね。それから本の通り、ターミナルの数を増やしたり、クライアントを落としてから再度立ち上げたりしてみましたが、今まで登録した内容はちゃんと残っています。めでたし。

ところで、ここで

  1. サーバーを落とす
  2. もう一度サーバーを立ち上げる
  3. クライアントから操作する

とすると、今までリマインダーに登録した情報は消えています。つまりリマインダーの情報はサーバー側が持っているのですね。ここをもうちょっと調べるために、通信の内容を間単に見てみます。

サーバー
192.168.0.4
ポート
9898
クライアント
192.168.0.2

としています。つまり、サーバーのURIは
druby://192.168.0.4:9898/
です。

クライアントから、

r = DRbObject.new_with_uri('druby://192.168.0.4:9898')
r.add 'foo'
r.add 'fooo'
r.add 'foooo'

としてみました。サーバーもクライアントもirbで起動しています。以下はそのパケットのダンプです。

(画像をクリックすると別窓に表示します)

送信のたびにデータ部(データ部とはTCPパケット内の、アプリケーションが自由に使えるデータが含まれる箇所の名前です。)のサイズが44,45,46と1byteずつ大きくなっているのは、addの引数の文字数の差だと思われます。サーバーから送られてくる15byteはメソッドの戻りだと思われます。
データの中身を見なくとも、送信されるデータが44byteと小さいですし、戻されるデータはリマインダーのデータ量に依存せず常に小さいので、本当にメソッド呼び出しだけしかしていない(クライアント側にリマインダーのフロントオブジェクトそのものが渡ってきているわけではない)ことがわかります。

 では次に、フロントオブジェクトそのものではなく、フロントオブジェクトが参照している他のインスタンス(@itemとか)について見てみます。サーバー側のirbでリマインダーに情報を追加してみます。

irb(main):008:0> r.add 'hogehoge'
=> 1

それからサーバー側でオブジェクトIDを見てみます。ちなみに、__id__で返される数値のことをここではオブジェクトIDと呼んでいます。これは、各オブジェクトの間で重複しないように付けられた識別番号(住民票コードのようなもの)です。もちろんプロセスが違っていれば同じ値が振られることもありますが、同一プロセスの中ではユニークになります。


irb(main):011:0> str = r[1]
=> "hogehoge"
irb(main):012:0> str.__id__
=> 21567584
irb(main):013:0> r[1].__id__
=> 21567584

ふむふむ、続いてクライアント側のirbで同様のことをしてみます。

irb(main):009:0> str = r[1]
=> "hogehoge"
irb(main):010:0> str.__id__
=> 21563792
irb(main):011:0> r[1].__id__
=> 21778008

 おや?クライアント側は、代入した時点で別のオブジェクトIDになっています。つまりstrとr[1]は別物です。これは多分、クライアント側の

irb(main):009:0> str = r[1]

 この行で、r[1]をサーバー側に問い合わせた時点で、オブジェクトの参照ではなく複製されたものが取得されるからでしょう。つまりr[1]は値渡しです。通常のRubyプログラミングと違いますね。このあたりは多分色々と難しいことが多そうなので、本を読み進めながら徐々に理解していきたいと思います。

 で、それから後ですが、ReminderCUIを作って遊び、「2.3 まとめ」をふむふむと読みました。はい、これで「2章:Hello, dRuby」は終わりにしたいと思います。結構長かったけど、dRubyの雰囲気は一応理解できた!つもりです。

 本だと、この次は「3章:eRuby」に続きますが、このサイトでは「3章:eRuby」を飛ばして「4章:参照渡しと値渡し」に入ります。「3章:eRuby」を飛ばす理由は、Webアプリケーションの話が入ってくると、多分dRubyそのもの以外に色々と面倒なことが出てきて混乱しそうだからです。このサイトでは、まずdRubyのことを学びたいので、Webアプリケーションで活用することは後回しにしたいと思います。

 「4章:参照渡しと値渡し」では、今日のこのエントリで見た付近の仕組みなども解説されているようなので、今から楽しみにしておきます。

 dRubyは結構脳ミソが刺激されていいです。

2007年09月25日

4章.参照渡しと値渡し (その1)

今日から4章(p.55~)に入ります。「参照渡しと値渡し(pass by reference, pass by value)」とは、今までにやった内容をさらに深く踏み込むようなタイトルがついている章ですね。この章で主に説明されているのはオブジェクトの渡し方についてです。すごくワクワクします。うん。それでははじまりはじまり。


Rubyの場合

  1. 参照渡し
  2. Rubyの場合は、基本は参照渡しです。本に記載されているスクリプトを抜粋します。

    % irb --prompt simple
    >> def foo(str); str.chop!; end
    >> my_str = "Hello, World."
    >> foo(my_str)
    >> my_str
    => "Hello, World"
    >> foo(my_str)
    >> my_str
    => "Hello, Worl"
    

    foo(my_str)とすると、my_strの内容が複製されて渡されるわけじゃなく、my_strの参照が渡されます。chop!ってのはStringクラスのメソッドで、文字列の最後の1文字を取り除きます。chop!は、渡されたオブジェクトの内容を書き換えますので、foo(my_str)でmy_strの方も変わっちゃうということです。(!がメソッド名の末尾についているやつは、大抵自分自身の内容を直接変えるやつのようです。)

    ちなみに、代入も参照の代入なので

    my_str = "Hello, World."
    my_str2 = my_str
    my_str2.chop!
    

    とすると、my_strも変わりますね。

  3. 値渡しその1:浅いコピー(shallow copy)
  4. 値渡しをしたいときは、dupで複製を作って渡すようです。以下も本からの抜粋です。

    >> my_str = "Hello, World."
    >> foo(my_str.dup)
    => "Hello, World"
    >> my_str
    => "Hello, World."
    >> foo(my_str.dup)
    => "Hello, World"
    >> my_str
    => "Hello, World."
    

    はい、出来ました。なお、dupはオブジェクトを複製するメソッドですが、浅くコピる(インスタンス変数などまでは複製しない)ようです。2章で本に登場したReminderクラスを使って試してみます。Reminderクラスとは、本のp.16に載っているreminder0.rbのことです。今回はRubyの実験なのでdRubyは使いませんよ。

    irb(main):002:0> r = Reminder.new
    => #<Reminder:0x2927ce4 @serial=0, @item={}>
    irb(main):003:0> r.add('hogehoge')
    => 1
    irb(main):004:0> r.to_a
    => 1, "hogehoge"
    irb(main):005:0> r2 = r.dup
    => #<Reminder:0x29a1abc @serial=1, @item={1=>"hogehoge"}>
    irb(main):006:0> r.__id__
    => 21577330
    irb(main):007:0> r2.__id__
    => 21826910
    irb(main):008:0> r[1].__id__
    => 21564760
    irb(main):009:0> r2[1].__id__
    => 21564760
    irb(main):010:0> r2.delete 1
    => "hogehoge"
    irb(main):011:0> r.to_a
    => []
    irb(main):012:0> r2.to_a
    => []

    r(Reminderのインスタンス)と、rをdupして作成したr2はそれぞれ別のIDになっていますが、インスタンス変数である@itemの各要素までは複製されていないため同じIDを返します。なので、r2で要素を消すとrにも影響を与えます。

  5. 値渡しその2:深いコピー(deep copy)
  6. Rubyで深いコピーを実現したいときには、Marshalを使うようです。Marshalはオブジェクトを文字列に書き出したり、それらからオブジェクトに復元したりするもの(モジュール)です(http://www.ruby-lang.org/ja/man/?cmd=view;name=Marshal

    先ほどirbで書いたスクリプトでdupしている部分をMarshalに変えてみます。

    irb(main):003:0> r = Reminder.new
    => #<Reminder:0x2921498 @serial=0, @item={}>
    irb(main):004:0> r.add('hogehoge')
    => 1
    irb(main):005:0> r.to_a
    => 1, "hogehoge"
    irb(main):006:0> r2 = Marshal.load(Marshal.dump(r))
    => #<Reminder:0x2999e0c @serial=1, @item={1=>"hogehoge"}>
    irb(main):007:0> r.__id__
    => 21563980
    irb(main):008:0> r2.__id__
    => 21810950
    irb(main):009:0> r[1].__id__
    => 21832900
    irb(main):010:0> r2[1].__id__
    => 21810890
    irb(main):011:0> r2.delete 1
    => "hogehoge"
    irb(main):012:0> r.to_a
    => 1, "hogehoge"
    irb(main):013:0> r2.to_a
    => []

    変えた部分は赤字の部分のみです。Marshalを使うとごっそり複製されるので、r2で要素を消してもrに影響がありません。

dRubyの場合

ではdRubyの場合はどうなのかと言うと

dRubyではRubyのクラスライブラリMarshalを用いて他のプロセスにオブジェクトを渡します。

4.1.2 dRubyでは?

dRubyの場合は、先ほど試してみたMarshalの仕組みを使って、オブジェクトを文字列化して転送しているようです。なので、値渡しということですね。今日はここまで、続きはまた次回!

2007年10月03日

4章.参照渡しと値渡し (その2)

はい、では4章の続きです。今度はdRubyでのオブジェクトの渡し方です。基本は本の通りに進めますが、「2章.Hello, dRuby.」で使ったReminderクラス(p.16のreminder0.rb)を使って試そうと思います。多分このクラスが一番馴染み深いと思うからね。じゃあはじめます。

dRubyの場合

dRubyではRubyのクラスライブラリMarshalを用いて他のプロセスにオブジェクトを渡します。 Marshalは任意のオブジェクトをバイト列に直列化するdumpメソッドと、直列化されたバイト列からオブジェクトを再現するloadメソッドからなります。 MarshalはRubyでdeep copy(深いコピー)を行うのにも利用できます。

4.1.2 dRubyでは?

とあるので、前回の最後に書いたとおり、dRubyの場合は値渡しということになりそうです。本ではここでMarshalの実験をしています。が、このサイトでは、まずReminderを使った別の実験をしてみます。というわけで、何回か前のエントリでやったように電文を見ます。今回はデータ部の中身までじろじろ見ていきます。TCPパケットは能弁に語る、気がする。

サーバー側は、2章で登場したサーバーをそのまま使います。クライアント側は

require 'drb/drb'
r = DRbObject.new_with_uri('druby://hoge')
r.add('foo')
str = r[1]

とやり、パケットをダンプしました。
まずは

r.add('foo')
を見てみます。この送信電文は以下の通りです。色が変わっている部分がTCPのデータ部です。

うーん、さっぱりわかんね。fooとかaddとか断片的にわかりますが、転送されるデータの内容(データ部の内訳)は謎です。仕方ないので送信部分のソースも見ていくことにします。

まず、DRbMessageの送信部分です。

def send_request(stream, ref, msg_id, arg, b) # :nodoc:
 ary = []
 ary.push(dump(ref.__drbref))
 ary.push(dump(msg_id.id2name))
 ary.push(dump(arg.length))
 arg.each do |e|
  ary.push(dump(e))
 end
 ary.push(dump(b))
 stream.write(ary.join(''))
・・・

です。最後につけられるbってのがわからなかったので調べました。これはmethod_missingの引数の一部みたいですが、どうやらブロック(do … end)が渡されたときにはこっちに入るようです。ブロックを渡すようなことを当面はしそうに無いので、今回はあまり深く考えずに置いておきます。
さらに、このメソッドの中で各所で呼ばれているdumpメソッドの内容も見てみます。

def dump(obj, error=false) # :nodoc:
 obj = make_proxy(obj, error) if obj.kind_of? DRbUndumped
 begin
  str = Marshal::dump(obj)
 rescue
  str = Marshal::dump(make_proxy(obj, error))
 end
 [str.size].pack('N') + str
end

ここで一番重要な処理は

[str.size].pack('N') + str

ではないかと思われます。strとは、オブジェクトを文字列に符号化したもの(マーシャルデータ)です。ここではそのstrの前に、strのサイズ(バイト数)を固定長のバイナリデータとしてくっつけています。リファレンスを見てみると、pack('N')とは、big endian unsigned 32bitの形式で、バイナリにパックするメソッドです。で、こいつはプラットフォームに非依存なので、エンディアンの変換は気にしなくてもいいということですね。マーシャルデータの前にサイズをつけているのは、どこまでを読んで復元してよいのかわからないからです。可変長のデータを送受信するときの常套手段かな(それか最後に、“ここまで”という記号をつけて送るかどちらか。)

これでデータ部をだいたい理解できるようになったはず。ではデータ部の先頭から順番に見ていきます。ここはオプション引数が入っているはずの部分です。

00 00 00 03 04 08 30

先頭から4バイトが、データ長をpack('N')したものです。03なので、ここから3バイトがオブジェクトの内容だと言う事だと思います。内容は04 08 30ですが、これはnilってことみたいです。実際に以下のようなコードを実行してみると同じ結果になります。

obj = nil
Marshal::dump(obj).each_byte do |b|
  puts sprintf("%x",b)
end

今回はオプション引数が無いのでnilのようです。もし「2章.Hello, dRuby (その5)」で試したようにオプション引数を使っていればその文字列が渡されてくるのだと思います。では次。

00 00 00 07 04 08 22 08 61 64 64

先頭4バイトは7なので、それに続く7バイトを見ます。04 08 22 08 61 64 64は'add'をマーシャルデータ化したものです。最後の61 64 64は、ASCIIで'add'ですね。04 08 22 08 はよくわかんないけど、文字列だよ、みたいなことを教えてる情報なんでしょうきっと。

続きも見ていきます。

00 00 00 04 04 08 69 06

これは、数値の1のマーシャルデータです。引数の個数を示します。

00 00 00 07 04 08 22 08 66 6f 6f

次に、上記の個数分だけ引数の内容が続きます。今回は、'foo'という文字列のオブジェクトのマーシャルデータです。(66 6f 6fが'foo'です。)複製されたオブジェクトが渡されています。なので確かに値渡しですね。

 ということで、以下がdRubyの(通常の)TCP送信時の電文形式のようです。各々先頭に、サイズを示す4バイトの固定長バイナリが付加されます。

1. オプション引数
2. メソッド(メッセージ)名
3. 引数の数
4. 引数の内容(3の数だけ存在)
5. ブロックの情報

以上です。次回は、サーバーからの応答(メソッドの戻り値)を見ていきたいと思います。

2007年10月04日

4章.参照渡しと値渡し (その3)

前回のエントリでは、メッセージ呼び出し時の引数について値渡しの確認をしました。今回は応答を見ます。メソッドの戻り値というやつです。前回と同様に2章で利用したリマインダーのdRubyサーバーを立ち上げておき、以下のスクリプトを実行してその中身を見ます。

require 'drb/drb'
r = DRbObject.new_with_uri('druby://hoge')
r.add('foo')
str = r[1]

このスクリプトのうち、前回は

r.add('foo')

を見ました。今回はその次のステップである

str = r[1]

を見ていくことにします。まず送信部分のTCPデータ部です。前回と同じように、色が変わっている部分がデータ部です。

見てわかるとおり、r[1]もメソッドです。([]というメソッドに、引数:1が渡される)ので、前回のadd('foo')の時と殆ど変わりはありません。解説は省略します。
次はサーバーから返されるデータ(クライアントが受信するデータ)のデータ部の内容を見ていきます。これも色が変わっている部分がデータ部です。

データ部は18バイトしかありません。短いですね。先頭から見ていきます。

00 00 00 03 04 08 54

これはboolのtrueを表します。リモートのメッセージパッシングが成功したらtrue。失敗したらfalseが入ってくるようです。

で、これに続くのが、

00 00 00 07 04 08 22 08 66 6f 6f

これは、'foo'という文字列のオブジェクトのマーシャルデータです。(66 6f 6fが'foo'です。)複製されたオブジェクトが渡されています。

なお、DRbMessageの以下のメソッドが、該当の処理を行なっている部分だと思います。

def send_reply(stream, succ, result) # :nodoc:
 stream.write(dump(succ) + dump(result, !succ))
rescue
 raise(DRbConnError, $!.message, $!.backtrace)
end

このことからdRubyのメソッドの戻り値の電文形式は以下の通りとわかりました。各々先頭に4バイトのサイズを示す固定長バイナリデータが付加される点は、送信時と同様です。

1. 成否のbool値
2. メソッドの戻り値のオブジェクト(複製)

この結果から、通常のdRubyのメッセージは、送受信共に確かに値渡しだということがわかりました。今回はここまでにします。次回は参照渡しをしてみます。

2007年10月11日

4章.参照渡しと値渡し (その4)

4章.参照渡しと値渡し (その4)

今回は参照渡しをしてみます。ちょっと本に戻ります。p.57の「4.1.2 dRubyでは?」からです。が、Marshalのあたりは、同じようなことを既に確認したような気がしますのでここでは省略します。本に沿って試してみて下さいませ。ここではその続き「p.58 参照の値渡し」から読みます。

Marshal.loadによって直列化したオブジェクトをMarshal.dumpで復元するということは、つまりオブジェクトの複製を作ることにほかなりません。ではdRubyはいつも値渡しなのでしょうか?
実は当たっているとも言えるし違うとも言えます。 dRubyでの参照渡しは、元のオブジェクトの代わりに参照情報を含むオブジェクトを直列化することで実現されます。つまり、参照渡しは参照オブジェクトの値渡しなのです。逆に、もう一方の値渡しは単純にオブジェクトを直列化するだけです。

4.1.2 dRubyでは?

ふむふむ。

Hello, World.の実験で使ったDRbObjectを覚えていますか? DRbObjectは元のオブジェクトを参照する情報をもったオブジェクトです。 DRbObjectには目的にごとに二つの生成方法が用意されています。一つはDRbObject.new_with_uriを使ってURIからリモートオブジェクトへの参照を作ることです。もう一つはDRbObject.new(obj)のようにオブジェクトを与え自プロセスのオブジェクトへの参照を作ることです。

4.1.2 dRubyでは?

ふむふむふむ。

DRbObject.new(obj)によって、任意のオブジェクトへの参照オブジェクトを作ります。参照を作るということは、他のプロセスがこの参照をつかってメソッド呼び出しする可能性があるということです。ですから、オブジェクトへの参照を作る前に、DRb.start_serviceによってdRubyのサーバを自プロセスで起動しておかなくてはなりません。

4.1.2 dRubyでは?

ふむふむふむふむふむ!!(こればっか)

元の文章でも充分わかりやすいですが、整理します。ここ、すごい重要な気がします。


  1. dRubyは、通常は値渡しだが、参照渡しにすることも出来る

  2. dRubyでの参照渡しは、厳密に言うと参照オブジェクトの値渡しだ

  3. 参照オブジェクトとは、すなわちDRbObjectのこと

  4. DRbObjectにはふたつの使い道がある

  5. 4-1. DRbObject.new_with_uriを使ってURIからリモートオブジェクトへの参照を作る
    4-2. DRbObject.new(obj)のようにオブジェクトを与え自プロセスのオブジェクトへの参照を作る

  6. 今までは上 (4-1) の使い道でしか使ってなかった。

  7. DRbObject.new(obj)で参照を作るためには、クライアントであってもDRb.start_serviceしないと駄目

  8. なぜなら、そのオブジェクトを他からリモート呼び出しすることがあるから(オブジェクトの実体はクライアントにあるから)


すげ!ちょっと感動します。

確かめてみます。p.60に掲載されているように、ハッシュを公開するサーバーを用意します。

% irb --prompt simple -r drb/drb
>> front = {}
>> DRb.start_service(nil, front)
=> #
>> DRb.uri
=> "druby://yourhost:1426"

はい立ち上げてみました。

前回の値渡しの復習をかねて、文字列をハッシュ(フロントオブジェクト)に登録してみます。

there = DRbObject.new_with_uri('druby://yourhost:1426')
str = "Hello, World."
there[1] = str

これは勿論うまくいく。では次に参照を渡してみます。参照はDRbObjectのことです。

there = DRbObject.new_with_uri('druby://yourhost:1426')
str = "Hello, World."
there[1] = str
there[2] = DRbObject.new(str)

しかしこれではうまくいきません。もしこのまま実行すると

in `current_server': DRb::DRbServerNotFound (DRb::DRbServerNotFound)

というエラーが出てしまいます。
なぜなら、DRb.start_serviceをしていないからです。クライアントからオブジェクトの参照を放り込んでいるということは実体がクライアントにあるということです。なのでメッセージに応答するのはクライアントです。クライアントが他局からのメッセージ呼び出しを受け付けることができないと駄目なので、dRubyのサービスが必要です。これがDRb.start_serviceをしなければならない理由です。

DRb.start_service
there = DRbObject.new_with_uri('druby://yourhost:1426')
str = "Hello, World."
there[1] = str
there[2] = DRbObject.new(str)

これでうまくいきます。
今日はここまで。次回は今回やった参照渡しの部分をもうちょっと詳しく見るつもりです。

2007年10月19日

[番外]咳さんに褒められていた件

おおおおおおおおお、わーい!ありがとうございます。


記事に出てくる質問に、一つ一つ応えていってよいものなのかしら。
I like Ruby too. - dRubyの正面がすばらしい件について

ぜひぜひ、気が向いたのだけでもいいですので~

そう言えば誰からもコメントとかないなあ。僕からのインタラクションがないからだと思いますが。

2007年10月24日

4章.参照渡しと値渡し (その5)

dRubyでの参照渡しの続きです。dRubyでの参照渡しは、厳密に言うと参照オブジェクトの値渡しであることは前回学びました。参照オブジェクトとはDRbObjectのことなので、まずDRbObjectの内容を見ていくようにしましょう。
DRbObjectには二つのインスタンス変数があります。


@ref

オブジェクトの実体のID(hoge.__id__したものです。)

@url

dRubyサーバーを特定するURI


DRbObjectをnewするときに何かオブジェクトを渡すと、それが@refに入ります。例えばirbでこんなことをやってみます。

[サーバー側]

irb(main):001:0> require 'drb/drb'
=> true
irb(main):002:0> uri = 'druby://サーバーのホスト名:9898'
=> "druby://サーバーのホスト名:9898"
irb(main):003:0> h = {}
=> {}
irb(main):004:0> DRb.start_service(uri, h)
=> #<DRb::DRbServer:0x295ecd0 ・・・
irb(main):005:0> str = 'foo'
=> "foo"
irb(main):006:0> str.__id__
=> 21680810
irb(main):007:0> ref = DRbObject.new(str)
=> #<DRb::DRbObject:0x29530b0 @ref=21680810, @uri="druby://サーバーのホスト名:9898">
irb(main):008:0> h[1] = ref
=> #<DRb::DRbObject:0x29530b0 @ref=21680810, @uri="druby://サーバーのホスト名:9898">

 最後から2つ目の行(:007)を見ます。ここで作成したDRbObjectのインスタンスの内容を確認します。@refは、引数に渡されたstrというオブジェクトのIDが設定されています。@uriは、start_service時に指定したuriが入っています。DRbObjectのインスタンスを作ると、dRubyサービスのURIとオブジェクトが自動的に紐付きます。

 なお、前回のエントリで確認したように、DRbObjectのインスタンスを作る前にstart_serviceしてないとdRubyサービスが無いのでエラーになります。ただ、すっかりおなじみのnew_with_uriだけは特殊で、指定したURIのフロントオブジェクトに対する参照をもらうだけなのでstart_serviceしてなくても大丈夫です。irbをもうひとつ立ち上げて、クライアント側から(new_with_uriを使って)サーバーのフロントオブジェクトを取得するスクリプトを見ます。このエントリではこの2つのirbをそのまま使い続け、どんどん追記していきます。どちらのことか判断しやすいように、各々[サーバー側]と[クライアント側]という風に明記することにします。

[クライアント側]

irb(main):001:0> require 'drb/drb'
=> true
irb(main):002:0> there = DRbObject.new_with_uri('druby://サーバーのホスト名:9898')
=> #<DRb::DRbObject:0x29a5f68 @ref=nil, @uri="druby://サーバーのホスト名:9898">

この場合、@refはnilです。本にも

@refがnilの場合は特別にURIに関連づけられたフロントオブジェクトを参照します。

4.1.2 dRubyでは?

と記載がありますね。ちなみに前にも見たかもしれないですが、DRbObjectのnew_with_uri(uri)は、DRbObject.new(nil, uri)と同じです。

# Create a new DRbObject from a URI alone.
def self.new_with_uri(uri)
 self.new(nil, uri)
end

では、クライアントのirbから操作を続けてみます。

[クライアント側]

irb(main):003:0> s = there[1]
=> #<DRb::DRbObject:0x29a59a0 @ref=21680810, @uri="druby://サーバーのホスト名:9898">

これは、先ほどサーバーで作成したDRbObjectです。@refは21680810ですね。サーバー側のオブジェクトIDが設定されています。参照オブジェクトに対する操作をクライアント側で引き続き行ってみます。

[クライアント側]

irb(main):004:0> s.upcase!

これをしてから、サーバー側のirbを操作して中身を確認してみましょう。

[サーバー側]

irb(main):009:0> str
=> "FOO"
irb(main):010:0> ref.to_s
=> "FOO"
irb(main):011:0> h[1].to_s
=> "FOO"

upcase!は、文字をすべて大文字に書き換えるメソッドです。はい、ちゃんとサーバー側のオブジェクトの実体(IDが21680810のオブジェクト)に変更が加えられていました。めでたし。

ところで、ここでちょっと疑問です。どうやらオブジェクトのIDを知っていることで、そのIDに紐付いたオブジェクトの実体を触れるようですが、いったいどのような仕組みになっているのでしょうか。仕組みを知るためにDRbServerのソースを見てみます。

# Convert a dRuby reference to the local object it refers to.
def to_obj(ref)
 return front if ref.nil?
 return front[ref.to_s] if DRbURIOption === ref
 @idconv.to_obj(ref)
end

ここで、今回関係ある部分は「@idconv.to_obj(ref)」です。@idconvは、デフォルトではDRbIdConvのインスタンスが設定されているようなので、このクラスを見ることにします。

class DRbIdConv

# Convert an object reference id to an object.
#
# This implementation looks up the reference id in the local object
# space and returns the object it refers to.
def to_obj(ref)
 ObjectSpace._id2ref(ref)
end
・・・

ここではObjectSpaceというものを使って変換しているようです。Rubyのリファレンスを見てみます。

ObjectSpace
全てのオブジェクトを操作するためのモジュール。

モジュール関数:
ObjectSpace._id2ref(id)
オブジェクト ID(Object#__id__)からオブジェクトを得ます。対応するオブジェクトが存在しなければ例外 RangeError が発生します。

Rubyリファレンスマニュアル - http://www.ruby-lang.org/ja/man/index.cgi?cmd=view;name=ObjectSpace

ObjectSpaceモジュールとは、どうやらRubyスクリプト内で実体化されたすべてのインスタンスの集合に対して色々できるやつのようです。なるほど、こういうやつが居るんですね。わかったので先へ進みます。

では、次はクライアントのirbから参照を渡します。ちゃんとstart_serviceしておきます。引数を指定しない場合、空いている適当なポート番号が割り当てられます。ここでは30273が割り当てられました。

[クライアント側]

irb(main):005:0> DRb.start_service
=> #<DRb::DRbServer:0x296448c ・・・・
irb(main):006:0> str = 'bar'
=> "bar"
irb(main):007:0> there[2] = DRbObject.new(str)
=> #<DRb::DRbObject:0x2937ce8 @ref=21696060, @uri="druby://クライアントのホスト名:30273">


そしてサーバー側で内容を書き換えます。

[サーバー側]

irb(main):012:0> h[2]
=> #<DRb::DRbObject:0x297898c @ref=21696060, @uri="druby://クライアントのホスト名:30273">
irb(main):013:0> h[2].upcase!
=> "BAR"

クライアント側の実体が書き換えられていることを確認します。

[クライアント側]

irb(main):008:0> str

=> "BAR"


はい、ちゃんと書き換えられていますね。最後に、ここでクライアントのirbを終わらせてから、クライアントが実体を持っている参照オブジェクトに対してサーバーから操作してみましょう。エラーになるはずです。

[サーバー側]

irb(main):014:0> h[2].to_s
DRb::DRbConnError: druby://クライアントのホスト名:30273 - #<Errno::EBADF: Bad file descriptor - connect(2)>

 DRbObjectについての理解がだいぶ深まったような気がします。なお、このエントリでは、本の中で紹介しているものとほぼ一緒の内容を試していますが、細かな手順が結構違っています。なのでこのエントリを読まれた方も是非一度、本の「4.1.2 dRubyでは?」に載っている手順に沿っておさらいしてみて下さい。次回はその続き、「4.2 自動的な参照渡し」に入るつもりです。

2007年10月29日

4章.参照渡しと値渡し (その6)

はい、今回は「4.2 自動的な参照渡し」から読み続けます。

4.2 自動的な参照渡し

参照渡しにするには、いつも明示的にDRbObject.new()しなくてはならないのでしょうか?なんだか面倒ですね。 dRubyでは明示的に参照渡しをしなくても、ライブラリが自動的に判断して参照渡しと値渡しを選択する仕組みが用意されています。

dRubyでは次の規則で参照渡しと値渡しを選択します。

  • Marshal.dumpできるものは値渡し
  • Marshal.dumpできないものは参照渡し

4.2 自動的な参照渡し

ほうほう、今までは、参照渡しにするために明示的にDRbObject.newをしていました。ですが、Marshal.dumpできないものは参照渡しとなるわけですね、なるほど。本の「4.2.1 can't dump」あたりを読みながら実験してみます。本ではMarshal.dumpできない$stdoutを使って試していますね、では、本の通りにやってみます・・・はい、できました。(はやっ)

4.2.1 can't dump

front[:stdout]はIOではなく、DRbObjectとなりました。 DRbObjectの@uriはターミナル2のDRb.uriです。 front[:stdout]のDRbObjectはターミナル2のオブジェクトを参照するオブジェクトであることがわかります。

何がおきたのでしょうか?ターミナル2から$stdoutをターミナル1に渡す時、 dRubyのライブラリはMarshal.dumpが失敗したことを捉えて値渡しの代わりに参照渡しを行なったのです。

4.2.1 can't dump

にゃるほど、多分これに該当する箇所としてはDRbMessageの

private
def make_proxy(obj, error=false)
 if error
  DRbRemoteError.new(obj)
 else
  DRbObject.new(obj)
 end
end

だと思います。みなさんも本の内容に沿って一緒にやってみて下さい。短いですがこのくらいで「4.2.1 can't dump」はおしまいにします。

4.2.2 DRbUndumped

では次。「4.2.2 DRbUndumped」に入ります。DRbUndumpedとは、Marshal.dumpは成功するからそのままやっちゃうと値渡しになるんだけど、参照渡しにしたい。ってときに使うもののようです。

いつも参照渡しとなるように、Marshal.dumpを失敗させるための Mixinモジュール DRbUndumped を用意しています。DRb::DRbUndumped(別名DRbUndumpedも使えます)は、クラスにincludeしたりオブジェクトにextendして使用するモジュールです。

4.2.2 DRbUndumped

こちらも本に載っている手順の通りにやってみます。簡単ですね。

foo.extend(DRbUndumped)

とすると、そのオブジェクトが参照渡しになります。また、

class Foo
 include DRbUndumped

とすると、そのクラスから生成されるオブジェクトは常に参照渡しになるようです。はい、よくわかりました。

でも、ちょっと気になりませんか。このDRbUndumpedモジュールは常にMarshal.dumpを失敗させるとのことですが、一体どんな方法で仕組みを使って失敗させているのでしょう。気になるのでDRbUndumpedのソースを見てみます。

  module DRbUndumped 
    def _dump(dummy)  # :nodoc:
      raise TypeError, 'can\'t dump'
    end
  end

これだけですか、なんでこれでMarshal.dumpできないようになるのか全然わかりません。だいたい_dumpって何?むぅぅ、、ってことでRubyのリファレンスマニュアルを見てみます。

Marshal.dump において出力するオブジェクトがメソッド `_dump' を定義している場合には、そのメソッドの結果が書き出されます。メソッド `_dump' は引数として再帰を制限するレベル limit を受け取り、オブジェクトを文字列化したものを返します。

ユーザ定義のMarshal

とありました。なるほど、Marshal.dumpの中で_dumpが必ず呼ばれるんですね。そりゃそうだろうなあ。Marshalの中を見て一応確かめてみることにします。。と、ソースを見ようと思ったらこいつはCで書かれているのか。marshal.cというソースファイルのようですね。Ruby本体のソースは全然構造や関数の用途を把握していないので全くわかんないけど。。。えーっと、_dumpはどこかなと

void
Init_marshal()
{
・・・
    s_dump = rb_intern("_dump");
・・・

いました。これですね。rb_internというのがよくわかんないけど、付近のコードを見ても定義を行なっているだけの雰囲気があるので、"_dump"という文字列に対応するような識別子がs_dumpに設定されて特定できるようになっているのでしょう多分。

じゃあ次にMarshal.dumpで呼ばれる部分にあたりをつけてみます。。と、あったあった、おそらくこの部分だと思います。

static VALUE
dump(arg)
    struct dump_call_arg *arg;
{
    w_object(arg->obj, arg->arg, arg->limit);
    if (arg->arg->dest) {
    rb_io_write(arg->arg->dest, arg->arg->str);
    rb_str_resize(arg->arg->str, 0);
    }
    return 0;
}

ここで呼ばれているw_objectの中も見てみます。この関数は、220行を超える結構長めの関数です。型などに応じて処理を分けるcaseがあるからデカいようですね。で、この中で

if (rb_respond_to(obj, s_dump)) {
    VALUE v;
    v = rb_funcall(obj, s_dump, 1, INT2NUM(limit));
・・・

としています。rb_respond_toは、おそらくRubyのrespond_to?と同じようなものでしょう。Rubyのrespond_to?は、引数の名前のメソッドを持っているかどうかを問うものです。で、rb_funcallは、名前からして処理を呼び出す(実行する)ものだと思われますから、ここでの処理は、

_dumpと言う名前のメソッドがobjにあれば、それを実行しなさい

と言うことだと思います。全て勝手な推測ばかりですが。一応_dumpを呼んでそうな気はしました。しかしRubyのソースを読めるようになるまでには多分かなりの時間がかかるだろうなあ。と思ってたら、これを見ると色々詳しくなるらしいと聞きました。が、まだ私は読めてません。


なんか今回はかなり脱線してしまいました。
ので、ちょっと纏めます。


今回のエントリのまとめ


  • Marshal.dumpできるものは値渡し、できないものは参照渡し

  • 明示的にDRbObject.newすると参照渡し

  • DRbUndumpedをくっつけたオブジェクトは参照渡し


今日のところはこれで終了です。次回は「4.3 未知のオブジェクトとDRbUnknown」に入りたいところですが、ページ数も短いのでさらっと読んだ後に「4章.参照渡しと値渡し」のまとめっぽい簡単なスクリプトを書きたいと思っています。次回で4章が終わる予定です。

2007年11月07日

4章.参照渡しと値渡し (その7)

 今回のエントリは、「4.3 未知のオブジェクトとDRbUnknown」です。前回のエントリでは4章全体のまとめも合わせて一つのエントリにすると言いましたが、(あわせると長くなりそうなので)やっぱり分けることにしました。

 はい、ではDRbUnknownって何なんでしょう、何のためにあるのでしょう。本を読みます。

DRbUnknownとは知らないクラスをMarshal.loadしてしまったときの例外を捉え、ロードできなかったオブジェクトの代わりにロードされるオブジェクトです。 DRbUnknownはロードに失敗した原因を、二つ保持しています。一つはロードに失敗したバッファ、もう一つは定義が不明なクラス名/モジュール名です。

それぞれ次のメソッドで問い合わせることができます。

DRbUnknown#buf
Marshal.loadに失敗した直列化されたオブジェクトのバッファ。
DRbUnknown#name
例外のメッセージから調べた、未知のクラス/モジュール名。
DRbUnknown#reload
もう一度Marshal.loadしてみる。

4.3 未知のオブジェクトとDRbUnknown

メモメモ。続きも読んでみます。

dRubyのライブラリは知らないクラスを受けとってしまっても、そのバッファを包んだ DRbUnknownオブジェクトを自動的に生成します。 DRbUnknownに対して元のオブジェクトのつもりでメソッドを呼ぶことはできませんが、DRbUnknownを回送することはできます。

4.3 未知のオブジェクトとDRbUnknown

 ほうほうです。Marshalを使うと、マーシャルデータからオブジェクトを復元するとき(Marshal.loadするとき)に、そのオブジェクトの型を知っていないと復元できないみたいです。dRubyではオブジェクトを渡すときにMarshalの仕組みを利用していますので、この挙動から影響を受けるんですね。

 DRbUnknownの中身をちょっとだけ見てみます。小さなクラスなので見やすいです。まずは_dumpメソッドから。

def _dump(lv) # :nodoc:
  @buf
end

_dumpはMarshalでのdump時に呼ばれるメソッドであることは前回のエントリで勉強しました。なので、DRbUnknownが別プロセスに渡されるときには、DRbUnknownが包んでいる元々のマーシャルデータが渡されます。で、受け取ったプロセスで復元できたらそれでよし、復元できなければそこでもDRbUnknownになります。ってことですね。

 ちなみに、@nameを設定している部分はここです。

def initialize(err, buf)
  case err.to_s
  when /uninitialized constant (\S+)/
@name = $1
  when /undefined class\/module (\S+)/
@name = $1
  else
@name = nil
  end
  @buf = buf
end

 例外の文字列から名前を取っているみたいです。$1ってのは、正規表現にマッチした最初の文字列が入ってる組み込み変数です。@nameがnilになることもあるのか。elseに入るのはどういうときなのかなあ、名前すらわからないエラーのときですよねきっと。多分marchal.cの


static VALUE
r_object0(arg, proc, ivp, extmod)
struct load_arg *arg;
VALUE proc;
int *ivp;
VALUE extmod;
{
・・・

のメソッドあたりで投げられている例外の大半が該当するのだと思います。実際にDRbUnknownが捕捉している例外の文字列はvariable.cから出るやつのようです。中身はややこしそうなので追わないことにしました。このくらいにして本を読み進めます。

DRbUnknownの機構によって、クラス定義を知らないオブジェクトを(メソッド呼び出しはできないが)保持しておくことが可能になります。

プロセス間でオブジェクトを交換するためのQueueを考えてみましょう。中継するQueueのサービスがpushされる可能性のある全てのクラス定義を事前に知らなくてはならないのは、難しいことがあります。 DRbUnknownはこういった局面で特に有用な機能です。

4.3 未知のオブジェクトとDRbUnknown

 本に書かれていることはよくわかります。プロセス間通信を行う上では、サービス側が全てのオブジェクトの型を知っておくことは非現実的であったり面倒であったりすることが多いです。なので、dRubyでは、マーシャルデータからオブジェクトを復元することが出来なくても、それを保持できる仕組みを提供しています。それがDRbUnknownです。

 ちょっとかなり端折り気味ですが、以下にまとめを書いてDRbUnknownは終わりにします。一度は本の内容にそって実験してみて下さいね。

DRbUnknownのまとめ


  • DRbUnknownオブジェクトはMarshal.loadが失敗したときに生成される

  • DRbUnknownオブジェクトは、オブジェクトに復元できなかったマーシャルデータの情報を保持する

  • DRbUnknownオブジェクトの状態では、(中身のオブジェクトの)メソッドを呼べない

  • DRbUnknownオブジェクトをdumpすると、包み込んでいるマーシャルデータがそのまま返される

  •  つまりdRubyでのプロセス間通信時には、気にせずそのままホイホイ渡せばいい
  • Marshal.loadが成功したら、DRbUnknownオブジェクトの中身のオブジェクトが取れる


次回こそ、4章を終わりにします。

2007年11月14日

4章.参照渡しと値渡し (その8)

今回は4章のおさらいとして、observer(observer.rb)で遊ぼうと思います。
observerってのは、Rubyに標準で同梱されているライブラリです。これはRubyでオブジェクト指向プログラミングのデザインパターンのひとつであるObserverパターンの概念を、簡単に実装できるようにしてくれるもののようです。このソースのコメントに書かれているexampleのスクリプトをいじってみることにします。exampleは以下の通りです。

require "observer"

class Ticker     ### Periodically fetch a stock price.
 include Observable

 def initialize(symbol)
  @symbol = symbol
 end

 def run
  lastPrice = nil
  loop do
   price = Price.fetch(@symbol)
   print "Current price: #{price}\n"
   if price != lastPrice
    changed         # notify observers
    lastPrice = price
    notify_observers(Time.now, price)
   end
   sleep 1
  end
 end
end

class Price      ### A mock class to fetch a stock price (60 - 140).
 def Price.fetch(symbol)
  60 + rand(80)
 end
end

class Warner     ### An abstract observer of Ticker objects.
 def initialize(ticker, limit)
  @limit = limit
  ticker.add_observer(self)
 end
end

class WarnLow < Warner
 def update(time, price)    # callback for observer
  if price < @limit
   print "--- #{time.to_s}: Price below #@limit: #{price}\n"
  end
 end
end

class WarnHigh < Warner
 def update(time, price)    # callback for observer
  if price > @limit
   print "+++ #{time.to_s}: Price above #@limit: #{price}\n"
  end
 end
end

ticker = Ticker.new("MSFT")
WarnLow.new(ticker, 80)
WarnHigh.new(ticker, 120)
ticker.run

簡単に解説します。
Tickerがイベント発生機です。ランダムに金額をfetchし、それをObserverに通知します。通知するためのメソッドがObservableモジュールに定義されていますので、それをincludeしておきます。
Tickerが発生したイベントは、各Observerに通知されます。通知されたイベントに反応するかどうかの判断は各Observer任せです。
WarnHighやWarnLowなどのクラスは、インスタンス生成時(initialize時)に、自分自身をObserverとしてTickerに登録します。

実行してみましょう。

Current price: 83
Current price: 75
--- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 75
Current price: 90
Current price: 134
+++ Sun Jun 09 00:10:25 CDT 2002: Price above 120: 134
Current price: 134
Current price: 112
Current price: 79
--- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 79
・・・

こんな感じに延々と出力されるはずです。

はい、ではこれにdRubyを使ってみます。ObservableとObserverのプロセスを別にして、プロセス間通信してみましょう。まずは何も考えず安直に作ってみます。わかりにくいので、各プロセスに、それぞれ便宜的に「Observable側」と「Observer側」という名前をつけることにしますね。

[Observable側]

require "observer"
require 'drb/drb'

class Ticker     ### Periodically fetch a stock price.
 include Observable

 def initialize(symbol)
  @symbol = symbol
 end

 def run
  lastPrice = nil
  loop do
   price = Price.fetch(@symbol)
   print "Current price: #{price}\n"
   if price != lastPrice
    changed         # notify observers
    lastPrice = price
    notify_observers(Time.now, price)
   end
   sleep 1
  end
 end
end

class Price      ### A mock class to fetch a stock price (60 - 140).
 def Price.fetch(symbol)
  60 + rand(80)
 end
end

class Warner     ### An abstract observer of Ticker objects.
 def initialize(ticker, limit)
  @limit = limit
  ticker.add_observer(self)
 end
end

class WarnLow < Warner
 def update(time, price)    # callback for observer
  if price < @limit
   print "--- #{time.to_s}: Price below #@limit: #{price}\n"
  end
 end
end

class WarnHigh < Warner
 def update(time, price)    # callback for observer
  if price > @limit
   print "+++ #{time.to_s}: Price above #@limit: #{price}\n"
  end
 end
end

ticker = Ticker.new("MSFT")
DRb.start_service('druby://[ホスト]:9191', ticker)
ticker.run

[Observer側]

require 'drb/drb'

class Warner     ### An abstract observer of Ticker objects.
def initialize(ticker, limit)
  @limit = limit
  ticker.add_observer(self)
 end
end

class WarnLow < Warner
 def update(time, price)    # callback for observer
  if price < @limit
   print "--- #{time.to_s}: Price below #@limit: #{price}\n"
  end
 end
end

class WarnHigh < Warner
 def update(time, price)    # callback for observer
  if price > @limit
   print "+++ #{time.to_s}: Price above #@limit: #{price}\n"
  end
 end
end

ticker = DRbObject.new_with_uri('druby://[ホスト]:9191')
low = WarnLow.new(ticker, 80)
WarnHigh.new(ticker, 120)

スクリプトのダウンロード
[Observable側] 1-1.rb
[Observer側] 1-2.rb


 とりあえずできたかな。ではこれで動かしてみます。まずObservable側を動かしてから、次はObserver側を動かします。無事動きました!Observable側のコンソールにObserver側から登録したオブジェクトの出力メッセージが出ています。うまくいきました。

 ・・・でも、よく考えると微妙です。まず、クラスの定義がどちらのプロセスにも重複して書いてあります。これを別のファイルにしてrequireしたとしても、Observable側が、扱うクラスをすべて知っておかなければならないって言うのは(Javaなどの型縛り言語なら当然だとしても)ちょっとRubyやdRubyの考え方からは外れるような気がします。あと、当然ですがObserver側はすぐ終了しちゃいますね。。で、実行結果のメッセージはすべてObservable側に出ている。で、今までに勉強したとおりdRubyは基本的に値渡しだから、WarnLowやWarnHighは、Observer側のインスタンスじゃなくて、それを複製したオブジェクトをObservable側が持ってしまってるって事です。これはまずいですね。Observer側のプロセスが持っているオブジェクトの内容を参照してもらわないと色々な場面で不便そうです。
そこで、


  • Observable側が、扱うクラスを知ってないと駄目なのはだるそう

  • Observable側にObserverのインスタンスが値渡しされているのは不便そう

の、この2点を解決してみたいと思います。じゃあ、まず、単純にObservable側から、Warnerクラスとその派生クラスたちの定義を消して動かしてみます。Observer側は変えませんよ。

[Observable側]

require "observer"
require 'drb/drb'

class Ticker     ### Periodically fetch a stock price.
 include Observable

 def initialize(symbol)
  @symbol = symbol
 end

 def run
  lastPrice = nil
  loop do
   price = Price.fetch(@symbol)
   print "Current price: #{price}\n"
   if price != lastPrice
    changed         # notify observers
    lastPrice = price
    notify_observers(Time.now, price)
   end
   sleep 1
  end
 end
end

class Price      ### A mock class to fetch a stock price (60 - 140).
 def Price.fetch(symbol)
  60 + rand(80)
 end
end

ticker = Ticker.new("MSFT")
DRb.start_service('druby://[ホスト]:9191', ticker)
ticker.run

スクリプトのダウンロード
[Observable側] 2-1.rb


では、動かします。・・・っと、あれれ?

(druby://[ホスト]:9191) ・・・observer.rb:126 :in `add_observer': observer needs to respond to `update' (NoMethodError)

エラーになっちゃいました。observer.rbのエラーになっている箇所を見てみます。

module Observable

 #
 # Add +observer+ as an observer on this object. +observer+ will now receive
 # notifications.
 #
 def add_observer(observer)
  @observer_peers = [] unless defined? @observer_peers
  unless observer.respond_to? :update
   raise NoMethodError, "observer needs to respond to `update'"
  end
  @observer_peers.push observer
 end

うーん、respond_to?で聞いた結果、updateってメソッドがないって言われています。なぜでしょう??仕方ないので、ここで、observerの中身を見てみます。ブレークポイントを置いたりしてみましょう。

<DRb::DRbUnknown:0x29a3e5c @buf="\004\bo:\fWarnLow\006:\v@limitiU", @name="WarnLow">

あらら、DRbUnknownってオブジェクトになっていますね。これは前回のエントリで学んだやつです。そうなのか、なるほど、そうですね、考えてみれば当然ですね。WarnLowなどのクラスをObservable側のプロセスは知りません。自分の知らないものが値渡しとして渡されてくると、Marshal.load時に失敗するので駄目ってことですね。なるほど、結局参照渡しにしないと駄目ってことか。では、Observer側を参照渡しに変えてみます。

[Observer側]

require 'drb/drb'

DRb.start_service
class Warner     ### An abstract observer of Ticker objects.
include DRbUndumped
def initialize(ticker, limit)
  @limit = limit
  ticker.add_observer(self)
 end
end

class WarnLow < Warner
 def update(time, price)    # callback for observer
  if price < @limit
   print "--- #{time.to_s}: Price below #@limit: #{price}\n"
  end
 end
end

class WarnHigh < Warner
 def update(time, price)    # callback for observer
  if price > @limit
   print "+++ #{time.to_s}: Price above #@limit: #{price}\n"
  end
 end
end

ticker = DRbObject.new_with_uri('druby://[ホスト]:9191')
low = WarnLow.new(ticker, 80)
WarnHigh.new(ticker, 120)
DRb.thread.join

スクリプトのダウンロード
[Observer側] 2-2.rb

3行変えました。


  • 参照渡しなので、DRb.start_serviceする

  • 参照渡しなので、DRbUndumpedをincludeする

  • 実体がこっちのプロセスにあるため、死ぬとまずいのでDRb.thread.joinで待機

これで実行しましょう・・・うまくいきました!メッセージもちゃんとObserver側に出ていますね!めでたしめでたし。なんとなく参照渡しと値渡しの感覚がつかめたような気がします。皆さんもRubyのライブラリをdRubyにしてみたりして遊んでみて下さい。結構簡単に出来ちゃうことに驚くと思います。

これで4章は終了です。