The dRuby Book

7.4 Removing Tuples Safely with TupleSpaceProxy

In the previous example, processes may not finish properly if the client process quits abnormally.

  ​[Terminal 1]​
  ​% ruby ts01.rb​
  ​[Terminal 2]​
  ​% ruby ts01shp.rb​
  ​## Ctrl-C​
  ​/usr/local/lib/ruby/1.9/drb/drb.rb:566:in `read': Interrupt​
  ​from /usr/local/lib/ruby/1.9/drb/drb.rb:566:in `load'​
  ​....​
  ​## Restart​
  ​[Terminal 2]​
  ​% ruby ts01shp.rb​
  ​[Terminal 3]​
  ​% ruby ts01c2h.rb​
  ​1819206320230345134827641756866458766071.........​

In this example, we quit the process with Ctrl-C while FactServer at ts01.sh is calling take. We lose some tuples because the take method has not finished yet (see Figure 39, Losing a tuple due to cancellation while calling the take method). This is a limitation of dRuby: it has no mechanism to notify when the calling process or thread terminated, so the remote method continues the process.

images/rinda_lost.png

Figure 39. Losing a tuple due to cancellation while calling the take method

To avoid this problem, there is a method called move in TupleSpace. It’s similar to the take method, but it does extra things to remove the tuple safely.

Using move(port, pattern, sec=nil, &block) moves a tuple to the specified port. It takes out a tuple like the take method does, but it first calls port.push(tuple) and then removes the tuple. If the port.push(tuple) operation fails, the operation becomes invalidated, and it doesn’t delete the tuple. This catches the failure of a tuple move and therefore avoids missing tuples.

The move method is rarely used on its own. The TupleSpaceProxy class provides a take method that calls move internally. Here is the definition of TupleSpaceProxy#take:

  class TupleSpaceProxy​
  def take(tuple, sec=nil, &block)​
  ​ port = []​
  ​ @ts.move(DRbObject.new(port), tuple, sec, &block)​
  ​ port[0]​
  end
  end

All methods inside TupleSpaceProxy are pretty much the same as those in TupleSpace. Once a TupleSpaceProxy object is generated, you can use it in the same way as you use TupleSpace.

Here’s an example usage of TupleSpaceProxy:

ts01shp.rb
  ​require 'rinda/rinda'
  ​​
  class FactServer​
  def initialize(ts)​
  ​ @ts = ts​
  end
  ​​
  def main_loop​
  ​ loop do
  ​ tuple = @ts.take({"request"=>"fact", "range"=>Range})​
  ​ value = tuple["range"].inject(1) { |a, b| a * b }​
  ​ @ts.write({"answer"=>"fact", "range"=>tuple["range"], "fact"=>value})​
  end
  end
  end
  ​​
  ​ts_uri = ARGV.shift || 'druby://localhost:12345'
  ​DRb.start_service​
  ​$ts = DRbObject.new_with_uri(ts_uri)​
  ​FactServer.new(Rinda::TupleSpaceProxy.new($ts)).main_loop​

Let’s try the same experiment that we did at the beginning of this section to compare the result. This time, ts01c2h.rb should return the value after terminating ts01sh.rb:

  ​[Terminal 1]​
  ​% ruby ts01.rb​
  ​[Terminal 2]​
  ​% ruby ts01sh.rb​
  ​## Ctrl-C​
  ​/usr/local/lib/ruby/1.999999999/drb/drb.rb:554:in `read': Interrupt​
  ​from /usr/local/lib/ruby/1.9/drb/drb.rb:554:in `load'​
  ​....​
  ​## Restart​
  ​[Terminal 2] % ruby ts01sh.rb​
  ​[Terminal 3] % ruby ts01c2h.rb​
  ​1819206320230345134827641756866458766071.........​

Missing tuples will cause problems that are hard to debug. When you expect your subsystems to restart or halt, you should use TupleSpaceProxy#take.