The dRuby Book

6.2 How Rinda Works

OK, we’ve discussed the high-level concepts. Let’s try them by writing some code.

Creating TupleSpace

In Linda, the tuplespace isn’t visible and is accessible only through six operations. In Rinda, the tuplespace is implemented as a class that is called Rinda::TupleSpace.

The following is an example of starting up a tuplespace under the druby://:12345 URI. dRuby automatically supplements the hostname, so the service will be provided under druby://[hostname]:12345. If you’re going to use the service under the same machine, you can also specify druby://localhost:12345.

ts01.rb
  ​require 'rinda/tuplespace'
  ​$ts = Rinda::TupleSpace.new​
  ​DRb.start_service('druby://localhost:12345', $ts)​
  ​puts DRb.uri​
  ​DRb.thread.join​

To use Rinda::TupleSpace, require rinda/tuplespace.

TupleSpace.new(timeout=60) creates the tuplespace. It takes timeout as an argument in seconds. When it reaches the timeout, it invokes a keeper thread to remove out-of-date tuples or raise an exception for out-of-date operations. The default is set to 60 seconds.

You can use Rinda::TupleSpace to synchronize among threads like Queue or Mutex. You can also publish it via dRuby.

Next you’ll find out about the basic operations of TupleSpace.

Writing to and Taking from TupleSpace

In Rinda, a tuple is an array of multiple values. You can put any objects into a tuple. You can even put Thread objects and Proc objects, but they are passed only by reference, so this may not be practical (for an interesting way to pass Proc, refer to Chapter 8, Parallel Computing and Persistence with Rinda).

Here are some examples of a tuple as an array:

  ​['abc', 2, 5]​
  ​[:matrix, 1,6, 3.14]​
  ​['family', 'is-sister', 'Carolyn', 'Elinor']​

write (equivalent to the “out” operation in Linda) writes tuples into tuplespace. take (equivalent to the “in” operation in Linda) reads a tuple by these arrays but also takes wildcard pattern matching. In C-Linda, a wildcard is expressed as *. In Rinda, it is expressed as nil. If you specify nil, it matches all elements of the object.

Let’s try a write and take operation for Rinda::TupleSpace (see Figure 31, Experimenting with the write and read operation).

images/d2rinda1.png

Figure 31. Experimenting with the write and read operation. A tuple is stored into and retrieved from tuplespace.

Begin by using the previous example, ts01.rb, to start up tuplespace under the druby://localhost:12345 URI in terminal 1.

  ​% ruby ts01.rb​

Start up irb in terminal 2, put the ["take-test", 1] tuple, and then retrieve the same tuple with the ["take-test", nil] wildcard.

  ​% irb -r drb --prompt simple​
  ​>> DRb.start_service
  ​>> $ts = DRbObject.new_with_uri('druby://localhost:12345')
  ​>> $ts.write(["take-test", 1])
  ​>> $ts.take(["take-test", nil])
  ​["take-test", 1]​

The next take operation should be blocked because you took out all tuples in $ts (see Figure 32, The take is blocked when there is no tuple to take).

images/d2rinda2.png

Figure 32. The take is blocked when there is no tuple to take.
  ​>> $ts.take(["take-test", nil])

Let’s open terminal 3 and put a tuple into $ts.

  ​% irb -r drb --prompt simple​
  ​>> DRb.start_service
  ​>> $ts = DRbObject.new_with_uri('druby://localhost:12345')
  ​>> $ts.write(["take-test", 2])

Now that you put a tuple that matches the pattern in terminal 2, terminal 2 should be unblocked, and you should get the prompt back.

  ​$ts.take(["take-test", nil])​
  ​=> ["take-test", 2]​
  ​>>​

A tuple doesn’t have to be unique. You can write many duplicate tuples into tuplespace. Let’s try to write ["take-test", 3] twice and then take them twice.

  ​>> $ts.write(["take-test", 3])
  ​>> $ts.write(["take-test", 3])
  ​>> $ts.take(["take-test", nil])
  ​["take-test", 3]​
  ​>> $ts.take(["take-test", nil])
  ​["take-test", 3]​

Did it work as expected?

As a next example, let’s write a service and a client program to calculate factorials (see Figure 33, Expressing the factorial request tuple and the result tuple as a service).

images/d2rinda3.png

Figure 33. Expressing the factorial request tuple and the result tuple as a service

We’ll use terminal 2 as a client and terminal 3 as a server.

  ​#[Terminal 2]​
  ​​
  ​>> $ts.write(['fact', 1, 5])
  ​>> res = $ts.take(['fact-answer', 1, 5, nil])

The client writes a tuple to request the factorial of 5 and takes the result. The client waits until the answer returns.

  ​#[Terminal 3]​
  ​>> tmp, m, n = $ts.take(['fact', Integer, Integer])
  ​>> value = (m..n).inject(1){|a, b| a * b}
  ​>> $ts.write(['fact-answer', m, n, value])

The server takes [’fact’, Integer, Integer] and writes its factorial result into a tuple. This tuple unblocks terminal 2.

  ​>> res = $ts.take(['fact-answer', 1, 5, nil])
  ​["fact-answer", 1, 5, 120]​
  ​>> puts res[3]
  ​120​

Here is the script version of what we’ve just done:

ts01s.rb
  ​require 'drb/drb'
  class FactServer​
  def initialize(ts)​
  ​ @ts = ts​
  end
  ​​
  def main_loop​
  ​ loop do
  ​ tuple = @ts.take(['fact', Integer, Integer])​
  ​ m = tuple[1]​
  ​ n = tuple[2]​
  ​ value = (m..n).inject(1) { |a, b| a * b }​
  ​ @ts.write(['fact-answer', m, n, value])​
  end
  end
  end
  ​​
  ​ts_uri = ARGV.shift || 'druby://localhost:12345'
  ​DRb.start_service​
  ​$ts = DRbObject.new_with_uri(ts_uri)​
  ​FactServer.new($ts).main_loop​
ts01c.rb
  ​require 'drb/drb'
  ​​
  def fact_client(ts, a, b)​
  ​ ts.write(['fact', a, b])​
  ​ tuple = ts.take(['fact-answer', a, b, nil])​
  return tuple[3]​
  end
  ​​
  ​ts_uri = ARGV.shift || 'druby://localhost:12345'
  ​DRb.start_service​
  ​$ts = DRbObject.new_with_uri(ts_uri)​
  ​p fact_client($ts, 1, 5)​

Let’s run the program. Make sure that your TupleSpace is up on terminal 1. If not, run ts01.rb again.

Next is the factorial client program in terminal 2. You may wonder why you have to start up the client program first rather than the server program (good question!). The client program will halt until the server program comes up.

  ​% ruby ts01c.rb​

Finally is the factorial server in terminal 3:

  ​#[Terminal 3]​
  ​% ruby ts01s.rb​

The client in terminal 2 should return an answer (120) once the server starts. Start the client again. This time, it returns the answer immediately.

  ​% ruby ts01c.rb​
  ​120​
images/d2rinda4.png

Figure 34. Splitting a factorial service request into multiple requests

The factorial server and the client communicate with each other via tuplespace. The client puts the request tuple and waits for the result tuple. The server takes out the request tuple and puts the result tuple. The processes are coordinated by exchanging tuples via tuplespace. Thanks to tuplespace as a central location, you can write a coordination program easily without worrying about the timing. Let’s change the client program to split a request with a large factorial request into multiple requests divided by a certain range (see Figure 34, Splitting a factorial service request into multiple requests).

ts01c2.rb
  ​require 'drb/drb'
  ​​
  def fact_client(ts, a, b, n=1000)​
  ​ req = []​
  ​ a.step(b, n) { |head|​
  ​ tail = [b, head + n - 1].min​
  ​ req.push([head, tail])​
  ​ ts.write(['fact', head, tail])​
  ​ }​
  ​​
  ​ req.inject(1) { |value, range|​
  ​ tuple = ts.take(['fact-answer', range[0], range[1], nil])​
  ​ value * tuple[3]​
  ​ }​
  end
  ​​
  ​ts_uri = ARGV.shift || 'druby://localhost:12345'
  ​DRb.start_service​
  ​$ts = DRbObject.new_with_uri(ts_uri)​
  # p fact_client($ts, 1, 20000)
  ​fact_client($ts, 1, 20000)​

This program splits a request of 20,000 factorials per range of 1,000. It writes all the requests as ([’fact’], head, tail), takes all the results, and sums them up. Here is an example output of the actual write request and the take response:

  ​$ts.write(['fact', 1, 1000])​
  ​$ts.write(['fact', 1001, 2000])​
  ​$ts.write(['fact', 2001, 3000])​
  ​....​
  ​$ts.take(['fact-answer', 1, 1000, nil])​
  ​$ts.take(['fact-answer', 1001, 2000, nil])​
  ​$ts.take(['fact-answer', 2001, 3000, nil])​
  ​....​

When there is only one server, the response speed doesn’t change much. It does take a bit more time because of the additional network overhead. However, what will happen if you increase the number of servers? You could start a server in different hosts. If you have a machine with a multicore CPU, then you can start multiple servers in the same machine and can distribute its computation power. This will speed up processing time. The following is an example of starting a factorial server in a different host:

  ​#[Terminal 4 in different host]​
  ​% ruby ts01s.rb druby://yourhost:12345​
  ​​
  ​#[Terminal 2]​
  ​% ruby ts01c2.rb druby://yourhost:12345​

By the way, did you notice that I commented out the p statement at the bottom of ts01c2.rb? I noticed that it takes more time to convert a huge Bignum into a string than to do the actual computation when I was benchmarking the result with various servers. If you’re interested only in computation speed, you should comment it out like I did. You want to uncomment and print the result only when you want to see the result set. Thanks to the tuplespace, the client doesn’t know whether the number of servers increases. It’s worth noting that you don’t have to change anything on the client side when you change the number of servers. This is one of the interesting aspects of Linda.

Let’s sum up the features of Rinda’s write and take operations (they’re the same for Linda):

In the next section, we’ll see how to use the read method.

Reading from TupleSpace

read is similar to take. It’s equivalent to Linda’s rd operation, and it returns the copy without removing the tuple from tuplespace. Let’s try it. (See Figure 35, Read operation.)

images/d2rinda5.png

Figure 35. Read operation. Like take, read will be blocked if there is no tuple.

Make sure your tuplespace is up with ts01.rb. Then connect to the tuplespace via irb.

  ​% irb -r drb --prompt simple​
  ​>> DRb.start_service
  ​>> $ts = DRbObject.new_with_uri('druby://localhost:12345')
  ​>> $ts.write(["read-test"])
  ​>> $ts.read(["read-test"])
  ​=> ["read-test"]​

The read method only reads a tuple; it doesn’t delete it from tuplespace. Let’s try it again.

  ​>> $ts.read(["read-test"])
  ​=> ["read-test"]​

It worked again. This is because the read method doesn’t take out a tuple. If you do take and read, it will be blocked.

  ​>> $ts.take(["read-test"])
  ​=> ["read-test"]​
  ​>> $ts.read(["read-test"])

As in the previous example, let’s try to see whether writing a tuple from terminal 3 unblocks this. Open a new terminal and write a tuple.

  ​$ irb -r drb -r rinda/tuplespace --prompt simple
  ​>> DRb.start_service
  ​>> $ts = DRbObject.new_with_uri('druby://localhost:12345')
  ​>> $ts.write(["read-test"])

This should have unblocked the terminal 2 you had open earlier.

  ​>> $ts.read(["read-test"])
  ​=> ["read-test"]​
  ​>>​

Unlike Linda, Rinda has a method called read_all. read_all returns copies of all matching tuples as arrays of arrays. If there are no matching tuples, it returns an empty array ([]).

  ​>> $ts.read_all(["read-test"])
  ​=> [["read-test"]]​
  ​>> $ts.take(["read-test"])
  ​=> ["read-test"]​
  ​>> $ts.read_all(["read-test"])
  ​=> []​
  ​>> $ts.write(["read-test", 1])
  ​>> $ts.write(["read-test", 2])
  ​>> $ts.read_all(["read-test"])
  ​=> []​
  ​>> $ts.read_all(["read-test", nil])
  ​=> [["read-test", 1], ["read-test", 2]]​
  ​>>​

This will be handy for debugging purposes.

Taking and Writing with timeout

“inp” and “rdp” are nonblocking versions of “in” and “rd” in Linda. While “in” waits for matching tuples to be in tuplespace, “inp” returns an error if there are no matching tuples. Even though Rinda doesn’t have “inp” and “rdp,” you can get the same effect by setting a timeout.

take(pattern, sec=nil)

Returns a matching tuple. Takes the timeout as a second argument in seconds. If nil is passed, it never times out. The default is set to nil.

read(pattern, sec=nil)

Returns a copy of the matching tuple. Takes the timeout as a second argument in seconds. If nil is passed, it never times out. The default is set to nil.

If you set the timeout to 0, then it returns immediately, like the “inp” operation in Linda. When take times out, it raises Rinda::RequestExpiredError. (Make sure you require rinda/tuplespace. Otherwise, you will get a DRb::DRbUnknownError: Rinda:: message.)

  ​#[Terminal 2]​
  ​>> $ts.take(["timeout-test"], 0)
  ​Rinda::RequestExpiredError: Rinda::RequestExpiredError​
  ​>> $ts.write(["timeout-test"])
  ​>> $ts.take(["timeout-test"], 0)
  ​=> ["timeout-test"]​

The read method also has a timeout as a second argument in seconds. When 0 is specified, it becomes the same as the “rdp” operation in Linda.

  ​#[Terminal 2]​
  ​>> $ts.read(["timeout-test"], 0)
  ​Rinda::RequestExpiredError: Rinda::RequestExpiredError​

You need to be aware of a certain behavior of timeout timing. The “keeper” thread is in charge of checking and clearing up expired operations, and it’s set to run every 60 seconds by default. This means expiration will not happen until the next time the keeper thread runs (except for 0—it gets executed immediately).

  ​>> $ts.take(["timeout-test"], 5)

This doesn’t block for exactly five seconds but raises an exception within the next sixty seconds. You can adjust the timing by changing the keeper thread interval. You can also set the interval in the TupleSpace generation parameter. If you set the interval too soon, it may cause performance problems, so don’t set it too short.

  ​>> $ts2 = Rinda::TupleSpace.new(5)
  ​>> $ts2.take(["timeout-test"], 5)

The preceding example generates TupleSpace with a five-second timeout and then runs $ts.take with the same timeout period.

Pattern Matching in TupleSpace

In Rinda, pattern matching of a tuple is done by comparing each element of an array using ===. The tuple and the pattern match when the number of elements of both are the same and the comparison of each element returns true on ===. This lets you match not only a simple nil wildcard but also complex patterns, such as regular expressions, classes, and instances.

Let’s take the tuple whose first element includes add or sub (regexp /add|sub/) and the second and the third element are integers (Integer). Let’s try this from terminal 2.

  ​>> $ts.take([/add|sub/, Integer, Integer])

Next, we’ll write a tuple. If you write a tuple whose second element is Float ([’add’, 2.5, 5]), it won’t match the take method you ran in terminal 2 because their classes are different and === doesn’t return true.

  ​>> $ts.write(['add', 2.5, 5])

Your take operation in terminal 2 should be still blocked. Next, write [’add’, 2, 5]. Now take should complete because it matches the request.

  ​# At terminal 1​
  ​>> $ts.write(['add', 2, 5])
  ​​
  ​..........​
  ​​
  ​# At terminal 2​
  ​>> $ts.take([/add|sub/, Integer, Integer])
  ​=> ["add", 2, 5]​

By using Regexp, Range, and Class, you can describe very complex patterns. You could use it as a database if performance isn’t a major concern (Rinda::TupleSpace searches groups of tuples linearly, so it will cause scalability issues).