The dRuby Book

7.1 Adding a Timeout in a Tuple

We often make mistakes. Scripts contain bugs. While writing a script, we tend to rerun the same process again and again as we modify it. What happens if we leave a tuple in tuplespace that shouldn’t be duplicated while we’re developing? When both the tuplespace and the application run in one process, we won’t leave obsolete tuples because both the tuplespace and the applications are restarted. However, the tuplespace process can contain old tuples only when the tuplespace and the application live in different processes and only the application process is restarted. In an ideal world, an application should clear the tuplespace before it stops, but this doesn’t always happen.

Timeout and Renewer Class

You can set a timeout on a tuple (see Figure 37, A tuple times out after thirty seconds and gets deleted automatically). When a tuple expires, it is automatically removed from the tuplespace. This avoids obsolete tuples left in tuplespace even when an application quits under unexpected circumstances. You can set the timeout in the second argument of the write method.

  ​@ts.write(tuple, sec)​

After it reaches the set period, the tuple is removed from tuplespace automatically. When nil is given, the timeout period becomes indefinite, and it doesn’t get deleted automatically. The default is set to nil.

  ​% irb -r rinda/tuplespace --prompt simple​
  ​>> ts = DRbObject.new_with_uri('druby://localhost:12345')
  ​=> #<Rinda::TupleSpace:0x007ff99b03ba00>​
  ​>> ts.read_all(['test'])
  ​=> []​
  ​>> ts.write(['test'], 30)
  ​=> #<Rinda::TupleEntry:0x007ff99b889f90>​
  ​>> ts.read_all(['test'])
  ​=> [["test"]]​
  ​>> sleep(30)
  ​=> 30​
  ​>> ts.read_all(['test'])
  ​=> []​

We can specify not only Integer and nil but also an object called Renewer. Renewer has a renew instance method. The Renewer object returns how long it wants to extend once renew is called. Once the renew method returns true, the tuple is expired and removed from tuplespace immediately. An application can adjust how long it wants to extend a tuple’s life span depending on its state by using Renewer.

images/d2rinda6.png

Figure 37. A tuple times out after thirty seconds and gets deleted automatically.

Rinda has a class called Rinda::SimpleRenewer. You can define SimpleRenewer easily.

  class SimpleRenewer​
  ​ include DRbUndumped​
  def initialize(sec=180)​
  ​ @sec = sec​
  end
  def renew​
  ​ @sec​
  end
  end

All this renew method does is return 180. The point is that this is being passed by reference because we included DRbUndumped. If you pass this SimpleRenewer as a second argument of the tuple’s write method, the renew method will be called every time a tuple reaches the timeout period.

If an application finishes without leaving a tuple, the tuplespace calls the renew method in the next timeout period. However, it will fail to call the method because the application that puts SimpleRenewer has already finished. The tuplespace catches that it failed to call the renew method and deletes the tuple. By doing this, the tuplespace deletes the tuple for the next timeout period even when an application fails to clear up the tuple (see Figure 38, When a process that holds SimpleRenewer finishes, it fails to update and the tuple is deleted).

images/d2rinda7.png

Figure 38. When a process that holds SimpleRenewer finishes, it fails to update and the tuple is deleted.

The tuple expiration timing interval depends on the number that the renew method returns. In other words, it’s possible for a tuple to stay on the tuplespace for a while after the application finishes. You can make this interval shorter, but making the interval too short may impact performance because Renewer requires dRuby method invocation for the periodic update.

Invalidation Using TupleEntry

In addition to the Renewer method, there is a way to invalidate a tuple. However, this method is a little difficult to use when TupleSpace and an application are in different processes, so I recommend using Renewer. When you call the write method of TupleSpace, the method actually returns a value called TupleEntry. You can operate the tuple you put into tuplespace via TupleEntry. TupleEntry has the following methods:

cancel

Set a tuple’s timeout period to a past value and invalidate the tuple. The tuple is deleted from tuplespace.

renew(sec_or_renewer)

Renew a tuple’s timeout period. You can provide either a new time period value in seconds or a Renewer object. If you specify nil, it sets the timeout to indefinite.

Here are some examples:

  ​@entry = @ts.write(['foo', 'bar])
  @entry.cancel

The cancel operation doesn’t fail even when the tuple is taken by some other process. TupleEntry is created inside TupleSpace during a write operation. If its associated tuple is taken, then TupleEntry is left and forgotten. One thing you have to be careful about is the life span of TupleEntry. This isn’t an issue when the process that allocates TupleSpace and the process that writes the tuple are the same, but it will cause a GC problem if they are different. If another process has a returning value of a write operation, it holds only DRObject—which is referenced by TupleEntry—and it doesn’t refer to TupleEntry itself. If take happens, then the object TupleEntry refers to doesn’t exist anymore, and the object is wiped by GC. There are two workarounds to avoid this.

You can accomplish the first workaround by putting the logic to control GC into a process that provides TupleSpace and avoids TupleEntry being garbage collected. You can accomplish the second at the application level by controlling TupleEntry only for the tuples that are less likely to be taken. However, both workarounds seem a bit cumbersome. If you know that multiple processes are to be involved, it’s better to use Renewer.