A book recently published The dRuby book - distributed and parallel computing with Ruby (translation of a Japanese book written by the author of the library itself). In this article I will try to give an overview of the chapters of the book related to the DRb library. If you want to get acquainted with the topic in more detail, the book can be
bought or
downloaded . At once I will say that I will not speak in this post about stream synchronization, nor about the Rinda library.
Suppose you are writing a system that works with more than one process. For example, you have a web server that runs tasks for a long time in the background. Or you just need to ensure that data is sent from one process to another and coordinated. For such situations and need a library DRb. It is written entirely in Ruby and is included in the standard library, so you can start working with it instantly. To connect it you just need to write
require 'drb'
The virtues of the DRb library for the most part stem from the dynamism of the Ruby language itself.
First of all, when spending minimal effort at the preparatory stage, then you work with objects without thinking about where they are located: in one process or in another. The library completely masks all the technical details from you.
Secondly, you are not obliged to strictly prescribe the interface. Any Ruby object can expose its interface to the outside - so you can either take advantage of the functionality of one of the standard classes like
Hash
or
Queue
, or you can create your own class with any interface. In addition, nothing prevents you from changing the interface directly during the execution, and even using
method_missing
to process any requests. And, of course, updating the server interface doesn’t affect the client at all unless the client calls methods that change the signature or behavior. Thus, the server and the client are as independent as possible.
And finally, the client is not even obliged to know the classes of objects that the server returns to him, he can use them without it. Thus, the server is free to hide as many details as he pleases.
But, of course, there are pitfalls, and there are plenty of them. Fortunately, dRuby is easy to understand, but the understanding of its structure allows most of the problems to be avoided. The documentation for this library, unfortunately, does not clarify a lot of things, so the article will be interesting for both beginners and people who have already worked with the library.
')
To check that everything works, let's open two irb terminals. I admit that I do not know how great the differences are in ruby ​​1.8, so let's agree that we are discussing version 1.9 (especially since 1.8 - it will soon cease to be supported, hurray!)
Conventionally, these two terminals are server and client. The server must provide a front object that will receive requests. This object can be any object: even an object of the built-in type, even a module with a specially created interface. In turn, the client connects to the server and interacts with this object.
Let's, for example, start the server in the first terminal and set up an ordinary array.
require 'drb' front = [] DRb.start_service('druby://localhost:1234', front) front << 'first'
Now connect the client. We recognize the first element of the array and write one more element into the array.
require 'drb' DRb.start_service remote_obj = DRbObject.new_with_uri('druby://localhost:1234') p remote_obj p remote_obj[0] remote_obj << 'second'
Now you can call
front[1]
from the first terminal and see that the string
'second'
is located there. And you can connect another client, and from it also operate on the same front-end object.
As you have already noticed, the server is started by the
DRb.start_service
command (watch the register!). The method takes as an argument a string with the address of the form
'druby://hostname:port'
and the frontal object. The front object is the object that will receive requests.
When the server starts in a separate script, you must write
DRb.thread.join
at the end of the script. The fact is that the DRb-server is started by a separate thread, and Ruby terminates the program as soon as the main thread has finished. Therefore, if the main thread does not wait for the DRb-server's stream to be closed, then both streams will end early and the server will immediately become unavailable. Be prepared for the fact that the execution of the
DRb.Thread.join
method blocks the current thread until the server is turned off.
In order to connect to the server, you must call the
DRbObject.new_with_uri
method and pass as an argument the address where the server is started. This method will return the proxy object
remote_obj
. Requests (method calls) to the proxy object are automatically transferred to the object on the remote server, the method called is executed there, and then the result is returned to the client that called the method. (However, not all methods are called on the server. For example, judging by the behavior, the
#class
method runs locally)
The meaning of the
DRb.start_service
at the client will be discussed a little later.
Let's still figure out how to execute the method of a remote object. To do this, calling the proxy object method serializes (marshals) the method name and argument list, transmits the string using the TCP protocol to the server, which deserializes the call arguments, executes the method on the front object, serializes the result and sends it back to the client. Everything looks simple. In fact, you work with a remote object in the same way as with a normal object, and many of the actions for remote execution of the proxy method and the server hide from you.
But not everything is so simple. Remote method call - pleasure expensive. Imagine that a method on the server "jerks" a variety of argument methods. This would have turned into the fact that the server and the client, instead of performing calculations, would have addressed the lion's share of time to each other using a relatively slow protocol (and it’s good if the two processes are located on the same machine). To prevent this, the arguments and the result between processes are passed by value, not by reference (the object marshaling stores only the internal state of the object, and does not know its
object_id
—
object_id
, the object will be serialized first, and then deserialized will be only a copy of the original object, but not the same object, so the transfer is automatically made on a copy). In Ruby, usually everything is passed by reference, and in dRuby, usually by value. Thus, if you execute
front[0].upcase!
on the server, the value of
front[0]
will change, and if you run
remote_obj[0].upcase!
, then you will get the first element in upper case, but the value on the server will not change, since
remote_obj.[](0)
is a copy of the first element. This call can be considered similar to the
front[0].dup.upcase!
However, you can always define the behavior of dRuby so as to pass arguments and the result by reference, but more on that later.
Now it's time to talk about the first problem. Not all objects can be marshaled. For example, Proc- and IO-objects, as well as threads (Thread-objects) can not be marshalized and transferred by copy. dRuby in this case comes as follows: if the marshalization has not worked, then the object is passed by reference.
So, how is the object passed by reference? Recall Sy. Pointers are used for this purpose. In Ruby, the role of the pointer is
object_id
. To pass an object by reference, an object of class
DRbObject
.
DRbObject
is, in fact, a proxy object for passing by reference. An instance of this
DRbObject.new(my_obj)
class
DRbObject.new(my_obj)
contains the
object_id
object
my_obj
and the URI of the server from which the object came. This allows you to intercept the method call and transfer it to the very object on the remote machine (or other terminal) to which the method was intended.
Let's make our server a method
def front.[](ind) DRbObject.new(super) end
And from the client we will start the code.
remote_obj.[0].upcase!
The new method
#[]
did not return a copy of the first element, but a link, so after performing the
upcase!
method
upcase!
the frontal object has changed, it is easy to check by executing, for example, the commands
puts remote_obj
or
puts front
from the client and server, respectively.
But every time writing
DRbObject.new
- laziness. Fortunately, there is another way to pass an object by reference, and not by value. To do this, it is enough to make the object unmarsable. This is easy to do, just add the
DRbUndumped
module to the object.
my_obj.extend DRbUndumped class Foo; include DRbUndumped; end
Now the object
my_obj
and all objects of class
Foo
will be automatically passed by reference (and
Marshal.dump(my_obj)
will produce
TypeError 'can\'t dump'
).
I will give an example that I met in practice. The server sets up a front hash in which the values ​​are tickets (from the inside, a ticket is a state machine). Then
remote_obj[ticket_id]
issues a copy of the ticket. But this does not allow us to change the state of the ticket on the server, only locally. Let's DRbUndumped into the
Ticket
class. Now we receive from a hash not a copy of the ticket, but a link to it - and any actions with it happen now not on the client, but directly on the server.
And now it's time to remember the promise and tell you why you need to
DRb.start_service
the client
DRb.start_service
. Imagine that you have an array of frontal objects on your server, as in the first example.
Now let the client call the
remote_obj.map{|x| x.upcase}
remote_obj.map{|x| x.upcase}
In fact, on the front object, the map method is invoked with a block argument. And he, as we remember, does not work out to marshal. So this block argument is passed by reference. The
map
method on the server will access it with the
yield
instruction, which means the client is a server! But since the client has to be a server from time to time, it means that he also has to start the DRb server using the
start_service
method. It is not necessary to specify the URI of this server. How it works from the inside I do not know, but it works. And as you have already noticed, the differences between the client and the server are smaller than it might seem.
There is a risk to stumble upon a new nuisance. Suppose a method returned a link (not a copy) to an object generated directly in the method. If the server did not save this object separately anywhere (for example, did not put it in a special hash), then the server has no link to it. The client on the remote machine has, but the server does not! Therefore, late or early, the
troll will come to you for the mail for this object will come GC - garbage collector. This means that after a while the link of the
DRbObject
type at the client will “fade out” and will point to nowhere. Attempting to access the methods of this object will cause an error.
Therefore, we must take care that the server stores references to the returned objects, at least until they are used by the server. There are several solutions for this:
1) save all returned objects passed by reference to the array — then the garbage collector will not collect them, because the reference is used;
2) to transfer to the client the link in the block. For example:
Instead of this code:
Ticket.send :include, DRbUndumped def front.get_ticket Ticket.new end foo = remote_obj.get_ticket foo.start foo.closed?
It should be written like this:
Ticket.send :include, DRbUndumped def front.get_ticket object_to_reference = Ticket.new yield object_to_reference end remote_obj.get_ticket do |foo| foo.start foo.closed? end
A valid local variable on the server cannot be collected by the garbage collector. This means that the link inside the block will be guaranteed to work.
3) The book describes another way - you need to hook into the process of creating a link at the stage of receiving an
object_id
object and try at this point to delay the garbage collection process anyway. You can automatically add an item to the hash and store the object forever (as you can guess, the memory will end sooner or later), you can store a link to the object and clear it manually, you can clear this hash every few minutes.
The latter method can be implemented by executing
require 'drb/timeridconv' DRb.install_id_conv(DRb::TimerIdConv.new)
before starting the server. For more information, see Chapter 11 of the book - Handling Garbage Collection. It seems to me interesting and maybe after reading it you will have new ways to use the manipulation of the garbage collection process. But still, I think that in practice it is better to use the second method - and provide links to the block. More reliable and clearer.
It remains to illuminate, probably the last moment. Suppose that you pass an object to
Foo
as a link. The client does not know about any class
Foo
and yet this does not prevent him from working with the object. In essence, the client operates with an object of the
DRbObject
class. Everything is as usual.
Now imagine that you are not transmitting a link, but a copy. Serialization on the server retains the state of the object and the name of its class. The client received the string and tries to deserialize it. Of course, this does not work for him, because the client cannot create an object of a nonexistent class
Foo
. Then deserialization will return an object of type
DRb::DRbUnknown
, which will store a buffer with a marshalized object. This object can be passed on (for example, to the task queue). You can also find out the name of the class, load the appropriate library with the class, and call the
reload
method — then another attempt will be made to deserialize. it
No, it’s still not the last moment. I promised not to write about synchronization, but still I will say a few words.
For distributed programming, synchronization of actions and the atomicity of operations are critical concepts. The server starts in a separate thread. And for each request to the server, a separate thread is automatically created in which this request is processed. So it is simply necessary to prohibit different threads from accessing the same information at the same time. So, programming distributed and parallel systems use:
1)
lock = Mutex.new; lock.synchronize{ do_smth }
lock = Mutex.new; lock.synchronize{ do_smth }
2)
MonitorMixin
standard library
MonitorMixin
3) classes of the standard library
Queue
,
SizedQueue
Good luck in using DRb! I hope I prevented someone from spending many hours trying to understand why the object does not change, even though you use the destructive method, how to get the method to work on the client, the receiving block, and why the resulting link worked-worked — and suddenly ceased.
However, in the book you will find much more, especially about the library of Rinda and its fellows.