Posted at

Ruby: dRuby(drb)

More than 3 years have passed since last update.


はじめに

dRuby(drb) は分散オブジェクトプログラミングのための標準添付ライブラリです。

Rinda はタプルスペースを扱う標準添付ライブラリです。いわば、Ruby 版の Linda です。

Ring は Rinda 上でネーミングサービスを提供します。

※本稿内容は dRuby(drb) のみです。(Rinda/Ring は本稿では割愛)


drb

pry を使って drb を試してみました。

まずは、分散オブジェクトの提供(サーバ)側です。

$ pry -r drb                                  # 'drb' を require する

:

# pry の応答は、必要がない場合省いています。

[1] pry(main)> front = [1,:foo,'hello'] # front オブジェクト(提供するオブジェクト)
[2] pry(main)> uri = 'druby://:12345' # サーバの URI
# (ホストを記述する例:'druby://192.168.1.101:12345')
[3] pry(main)> DRb.start_service(uri, front) # サービス開始(サーバはスレッドで動く)

リモートからアクセスする場合、TCPポート(ここでは12345)がファイアフォールなどでブロックされているとアクセスできません。

また、スクリプトで記述する場合、(サーバスレッドが動いているのに)メインスレッドが終了してプロセスが消えてしまわないよう対処します。

例えば、以下のようにします。

DRb.thread.join           # サーバスレッドを join する(終了を待つ)


分散オブジェクトを提供される(クライアント)側です。

$ pry -r drb                                  # 'drb' を require する

:

[1] pry(main)> DRb.start_service              # サービス開始 (URIなし)

# DRbObject インスタンスを作る。この時サーバの URI を指定する
[2] pry(main)> array = DRbObject.new_with_uri('druby://:12345')
[3] pry(main)> puts array # array はリモートオブジェクト
1
foo
hello

ここで、array は分散オブジェクトです。Array ではありません。

[4] pry(main)> array.class

=> DRb::DRbObject # Array クラスではない

array (DRbObject) はリモートオブジェクトのプロキシ(代理人)です。自分に投げられたメソッド呼び出しをリモートの Array に転送しています。

ただし、array 自身が知っているメソッドには即答します(例えば上の class メソッド)。

[5] pry(main)> array.count

=> 3

[6] pry(main)> array.last
=> "hello"

[7] pry(main)> array.map {|x| "data is #{x}" }
=> ["data is 1", "data is foo", "data is hello"]

[8] pry(main)> p array # p (暗黙の inspect 呼び出し) には array 自身が答える
<DRb::DRbObject:0x007f54d7a91748 @uri="druby://:12345", @ref=nil>

[9] pry(main)> class << array # (あくまで、実験ですが)
[9] pry(main)* undef inspect # array.#inspect を undef してしまえば
[9] pry(main)* end # array 自身で答えられなくなり ...

[10] pry(main)> array.inspect # リモートの Array の応答が得られます
=> "[1, :foo, \"hello\"]"

[11] pry(main)> p array # (同じく)
[1, :foo, "hello"]


分散オブジェクトしてみる

自作クラスのインスタンスを作成して frontオブジェクト(Array)に格納してリモートで取得してみます。

まず、分散オブジェクトの提供(サーバ)側に戻ります。

[4] pry(main)> class C                      # クラスを定義

[4] pry(main)* include DRb::DRbUndumped # !!! これがキモです !!!
[4] pry(main)* attr_accessor :foo
[4] pry(main)* end

[5] pry(main)> obj = C.new # インスタンス作成

[6] pry(main)> obj.foo = "hello" # obj.foo に "hello" を設定

[7] pry(main)> front << obj # obj を front に追加
=> [1, :foo, "hello", #<C:0x007f7c124b4bd8 @foo="hello">]

分散オブジェクトを提供される(クライアント)側に戻ります。

[12] pry(main)> p array               # インスタンスが追加されているのを確認してみる

[1, :foo, "hello", #<C:0x007f7c124b4bd8 @foo="hello">]

[13] pry(main)> p array.last.foo # foo を取得
"hello" # "hello" が取得できた

[14] pry(main)> robj = array.last # 分散オブジェクト取得

[15] pry(main)> robj.foo = "goodbye" # foo に "goodbye" を設定

[16] pry(main)> p robj.foo # foo の 確認
"goodbye"

分散オブジェクトを提供する(サーバ)側に戻ります。

クライアント側で設定した "goodbye" が取れることを確認します。

[8] pry(main)> p front

[1, :foo, "hello", #<C:0x007f7c124b4bd8 @foo="goodbye">]

[9] pry(main)> p front.last.foo # obj.foo を取得
"goodbye" # "goodbye" が取得できた

上とは反対に、クライアント側でクラス定義して作成したインスタンスを array に追加して、サーバ側で取得すれば同じようにリモートオブジェクトとして扱えます。

その場合は、サーバ側で取得したリモートオブジェクトがメソッド呼び出しを相手に転送して処理をします。

リモート側でも DRb.start_service (サービス(サーバスレッド)開始)が必要なのはそのためです。(その分散オブジェクトに関してはサーバとクライアントの立場が逆になります。)


DRb::DRbUndumped

以下、dRuby のリモートへのオブジェクトの渡し方についてです。


  • 値渡しと参照渡しがある(分散オブジェクトとしてメソッド転送するのは参照渡し)


  • Marshal.#dump できるものが値渡しになる (ただし、front オブジェクトは参照渡し)


  • include DRb::DRbUndumped したクラスのインスタンスは Marshal.#dump に失敗する

1 や :foo やルックアップ専用の場合の String/Hash/Array などは値渡しでもいいと思います。

しかし、オブジェクトは分散オブジェクトとして機能させたい場合が多いと思います。

その場合は参照渡しにします。その為には Marshal.#dump できなくなる必要があります。

include DRb::DRbUndumped すると、Marshal.#dump に失敗するようになります。

[1] pry(main)> class C

[1] pry(main)* attr_accessor :foo
[1] pry(main)* end

[2] pry(main)> obj = C.new

[3] pry(main)> Marshal.dump(obj) # Marshal.#dump できてしまう
=> "\x04\bo:\x06C\x00"

[4] pry(main)> class C # DRb::DRbUndumped を include
[4] pry(main)* include DRb::DRbUndumped
[4] pry(main)* end

[5] pry(main)> Marshal.dump(obj) # Marshal.#dump に失敗(例外があがる)
TypeError: can't dump
from /home1/lnznt/.rbenv/versions/2.1.5/lib/ruby/2.1.0/drb/drb.rb:389:in `_dump'


例えば、値渡しの場合には、以下のようなことが起こります。

サーバ側 pry (front は Hash ひとつの Array。 Hash {} は値渡しになる)

[1] pry(main)> DRb.start_service 'druby://:12346', [{}]

クライアント側 pry

[1] pry(main)> array = DRbObject.new_with_uri 'druby://:12346'

[2] pry(main)> puts array[0] # 内容を確認
{} # 空の Hash

[3] pry(main)> array[0][:foo] = 'hello' # キー :foo に値 "hello" 格納

[4] pry(main)> puts array[0] # 再度、内容確認
{} # !!! 変わっていない !!!

(意図してではない場合、)エラーがでるわけでもないので、結構分かりにくいバグになると思います。

対して、参照渡しの場合です。

サーバ側 pry (Hash {} は参照渡し。分散オブジェクトとして機能する)

             # ここでは DRb::DRbUndumped は Hash インスタンスに直接 extend する

[1] pry(main)> DRb.start_service 'druby://:12347', [{}.extend(DRb::DRbUndumped)]

クライアント側

[1] pry(main)> array = DRbObject.new_with_uri 'druby://:12347'

[2] pry(main)> puts array[0] # 内容を確認
{} # 空の Hash

[3] pry(main)> array[0][:foo] = 'hello' # キー :foo に値 "hello" 格納

[4] pry(main)> puts array[0] # 再度、内容確認
{:foo=>"hello"} # ちゃんと変わっている


URI とリモートオブジェクトID

drb サービスは TCP を使用していますがコネクション張りっぱなしというわけでなく転送が必要になる度に接続するようです。その為、相手側が落ちていてもエラーになるのは実際に転送がかかる時です。

DRbObject は URI とリモートオブジェクトID で接続先とリモートのオブジェクトを識別します。

[14] pry(main)> robj = array.last     # 分散オブジェクト取得

=> #<DRb::DRbObject:0x007f54d8635978
@ref=70085429798380, # これがリモートオブジェクトID
@uri="druby://127.0.0.1:12345">

ちなみに、front オブジェクトの @ref は nil です。


おわりに

本稿内容の動作確認は以下の環境で行っています。


  • Ruby 2.1.5 p273