Project Spotlight: Rinda

Rinda is a built-in Ruby library to implement the Linda distributed computing paradigm in Ruby. According to Wikipedia: “Linda is a model of coordination and communication among several parallel processes operating upon objects stored in and retrieved from shared, virtual, associative memory.”

In this blog post, we will describe Rinda and show you how to use Rinda’s TupleSpaces and RingServers.

Intro to DRb

Distributed Ruby (aka DRb) is a library to allow communication between Ruby processes. The processes can be located on the same machine or over a network. Using DRB, one Ruby process can use a remote object located in another Ruby process as if it was local. One of the best introduction to DRb that you will find online can be found at segment7.net. I recommend reading this before proceeding with this post.

Basics of Rinda

Rinda implements a tuple space: a repository of tuples that can be accessed concurrently. Think of a tuple as a Ruby array. For example: [:add, 2, 5]

There are 5 basic operations that can be executed on a tuple space:

  • write: write a tuple to the tuple space. This take an optional timeout parameter. After that timeout expires, the tuple is automatically deleted from the space.
  • take: read and delete a tuple from the tuple space atomically.
  • read: simply read the tuple, leaving it on the tuple space.
  • read_all: read all the matching tuples from the tuple space without removing them.
  • notify: register for notifications of events such as ‘write’, ‘take’, or ‘delete’ (a delete occurs when a timeout expires)

For the read/write/read_all operations, you have to pass a template to the tuple space, which will be matched with the tuples present in the space. You can use nil as a wildcard when matching. Below are some examples of matches and non-matches:

  • [:add, 1, 2] matches [:add, 1, 2]
  • [:token, "gyuioasdg567890knhu"] matches [:token, nil]
  • [1,2,3,4,5] matches [nil,nil,nil,nil,nil]
  • [:token, :good, :friday] does not match [:token]
  • [:add, 4, 5] does not match [:add, 4, 5, 6]
  • [:renew] matches [nil]

The Rinda library has a Tuple class which is basically a thin wrapper around an array or a hash:

require 'rinda/rinda'

tuple1 = Rinda::Tuple.new([:add, 2, 4, 5])
puts "We created a tuple of size: #{tuple1.size}"

tuple2 = Rinda::Tuple.new({'a' => 'b', 'c' => 'd'})
puts "We created a tuple of size: #{tuple2.size}"

The library also ships with a Template class that you can use to match tuples:

Template.new([:foo, 5]).match   Tuple.new([:foo, 5]) # => true
Template.new([:foo, nil]).match Tuple.new([:foo, 5]) # => true
Template.new([String]).match    Tuple.new(['hello']) # => true

Template.new([:foo]).match      Tuple.new([:foo, 5]) # => false
Template.new([:foo, 6]).match   Tuple.new([:foo, 5]) # => false
Template.new([:foo, nil]).match Tuple.new([:foo])    # => false
Template.new([:foo, 6]).match   Tuple.new([:foo])    # => false

The library also ships with a TupleEntry class and a TemplateEntry class. These two classes add expiry and cancellation data to the basic Tuple and Template classes and are used by the Tuple Space implementation of Rinda.

Note that you will never have to use the Tuple, Template, TupleEntry, TemplateEntry classes when using Rinda. These are used internally by the library. I just wanted to cover these so that you have an idea of how Rinda is written. The code for Rinda is really simple and it can be foung in three files in the ruby source code: rinda/rinda.rb, rinda/tuplespace.rb, and rinda/ring.rb.

The TupleSpace class

When using Rinda, the one class that you will deal with is the TupleSpace class. It implements the 5 methods we talked about earlier: write, take, read, read_all, and notify.

It is very easy to use:

require 'rinda/tuplespace'

ts = Rinda::TupleSpace.new

DRb.start_service('druby://localhost:1234', ts)

DRb.thread.join

It really takes only 4 lines of code! On the second line, we are creating the tuple space. On the third line, we start the DRb service and put our tuple space at the following drb url: ‘druby://localhost:1234′. Finally, the last line joins the thread. Save these 4 lines of code in tuplespace.rb, just run it with: ruby tuplespace.rb. Now you can start writing agents that write or read to the tuple space that you are hosting at port 1234.

require 'rinda/tuplespace'

DRb.start_service
ts = Rinda::TupleSpaceProxy.new(DRbObject.new_with_uri('druby://localhost:1234'))

ts.write([:add, 1, 2])
ts.write([:add, 2, 3])

all = ts.read_all([:add, nil,nil])
puts all.size # => 2

tuple = ts.take([:add, nil, nil])
puts tuple.inspect # => [:add, 1, 2]

tuple = ts.take([:add, nil, nil])
puts tuple.inspect # => [:add, 2, 3]

all = ts.read_all([:add, nil,nil])
puts all.size # => 0

Save this code in a file called agent.rb and run it with ruby agent.rb. You should get the following output:

2
[:add, 1, 2]
[:add, 2, 3]
0

RingServer

Rinda also ships with an implementation of a ring server (which can be found in rinda/ring.rb in the ruby source). A RingServer allows a Rinda::TupleSpace to be located via UDP broadcasts. It allows DRb services (such as Tuple Spaces) to be found automatically by clients or agents.

If we want our TupleSpace to be found automatically using a RingServer, a better way to start it is using the following code:

require 'rinda/ring'
require 'rinda/tuplespace'

DRb.start_service

ts = Rinda::TupleSpace.new
Rinda::RingServer.new ts

DRb.thread.join

If you compare this code, to the code we wrote earlier to start the tuple space, you will notice that we did not have to provide a DRb URI. We use a RingServer instead. That way, when agents wants to connect to the tuple space, they can locate the tuple space using a UDP broadcast message. Of course, all of this is hidden from you and all you have to write to start using this tuple space is:

require 'rinda/ring'

DRb.start_service

ts = Rinda::RingFinger.primary

ts.write([:add, 1,2])

While you can use the primary tuple space directly to do your work. It is generally used to hold a tuple space that will serve as a name server to advertise DRb services. Other tuple spaces can register with this ring server to advertise their services and clients can ask the ring server which services are available.

To register itself as a service, a client writes a tuple to the tuple space that maches the format: [:name, class, the DRBObject to register, a description of the service]. For example, a tuple space would register by writing the following tuple on the Ring Server: [:name, :TupleSpace, tuplespace_object, 'Tuple Space'].

Rinda comes with a RingProvider class which makes it easy to provider a service and register with the ring server:

require 'rinda/ring'
require 'rinda/tuplespace'

DRb.start_service
ts = Rinda::TupleSpace.new

provider = Rinda::RingProvider.new :TupleSpace, ts, 'Tuple Space used to do math'
provider.provide

DRb.thread.join

To find services, a client/agent simply has to look at the tuples in the ring server:

require 'rinda/ring'

DRb.start_service

ring_server = Rinda::RingFinger.primary
all_services = ring_server.read_all([:name, nil,nil,nil])

puts "Found #{all_services.size} services."
all_services.each do |svc|
  puts "\nService Name: #{svc[1]} \nService Description: #{svc[3]}"
end

As you can see, Rinda makes it easy to create tuple spaces and advertise them using a ring server. Keep in mind that you can register any DRb service on the ring server, not just tuple spaces.

Conclusion

Rinda provides an interesting paradigm to distributed computing which is becoming more important because chip manufacturers have stopped increasing the speed of their processors and are instead shipping chips with many cores. This will require a shift in the way we write software. Software developers will need to write applications that can easily be distributed across many cores if they want to take advantage of these new chips. For an interesting paper on the subject, check out ‘The free lunch is over’ by Herb Sutter.

2 comments so far

  1. Bob Aman on

    I don’t think I’d recommend actually using Rinda. Pretty much everything you can do with Rinda, you can do better with something like RabbitMQ.

  2. ecin on

    While RabbitMQ does serve as a better messaging protocol, it requires additional setup. There’s a benefit to be had in using Ruby’s standard library, namely that you already have all you need out of the box.

    In other news, I find this as a plausible alternative to using Bonjour/Zeroconf for situations that just require communication between Ruby processes.


Leave a reply