The dRuby Book

1.2 Building the Reminder Application

Let’s create a simple task list application in which anyone can create, read, and delete entries. To keep it simple, the user interface is irb.

Let’s define the Reminder class first. It has three methods: Add, delete, and to_a. Each item has a unique ID that’s used when deleting an item.

reminder0.rb
  class Reminder​
  def initialize​
  ​ @item = {}​
  ​ @serial = 0​
  end
  ​​
  def [](key)​
  ​ @item[key]​
  end
  ​​
  def add(str)​
  ​ @serial += 1​
  ​ @item[@serial] = str​
  ​ @serial​
  end
  ​​
  def delete(key)​
  ​ @item.delete(key)​
  end
  ​​
  def to_a​
  ​ @item.keys.sort.collect do |k|​
  ​ [k, @item[k]]​
  end
  end
  end

Let’s start the Reminder server via irb. We’ll run it in terminal 1.

  ​# [Terminal 1]​
  ​% irb --prompt simple -I . -r reminder0.rb -r drb/drb​
  ​>> front = Reminder.new​
  ​>> DRb.start_service('druby://localhost:12345', front)​
  ​>> DRb.uri​
  ​=> "druby://localhost:12345"​

Next we’ll use irb in terminal 2 as a client to this server.

  ​# [Terminal 2]​
  ​% irb --prompt simple -r drb/drb​
  ​>> r = DRbObject.new_with_uri('druby://localhost:12345')​
  ​>> r.to_a​
  ​=> []​
  ​>> r.add('13:00 Meeting')​
  ​=> 1​
  ​>> r.add('17:00 Status report')​
  ​=> 2​
  ​>> r.add('Return DVD on Saturday')​
  ​=> 3​
  ​>> r.to_a​
  ​=> [​
  ​[1, "13:00 Meeting"],​
  ​[2, "17:00 Status report"],​
  ​[3, "Return DVD on Saturday"]​
  ​]​

Let’s start another client in a third terminal session and add and delete items.

  ​# [Terminal 3]​
  ​% irb --prompt simple -r drb/drb​
  ​>> r = DRbObject.new_with_uri('druby://localhost:12345')​
  ​>> r.to_a​
  ​=> [​
  ​[1, "13:00 Meeting"],​
  ​[2, "17:00 Status report"],​
  ​[3, "Return DVD on Saturday"]​
  ​]​
  ​>> r.delete(2)​
  ​>> r.to_a​
  ​=> [[1, "13:00 Meeting"], [3, "Return DVD on Saturday"]]​
  ​>> r.add('15:00 Status report')​
  ​>> r.to_a​
  ​=> [​
  ​[1, "13:00 Meeting"],​
  ​[3, "Return DVD on Saturday"],​
  ​[4, "15:00 Status report"]​
  ​]​

The two clients are accessing the same shared data. This is because both terminals 2 and 3 share the same Reminder object, which exists on the server (terminal 1). The clients both have a remote reference to them (see Figure 3, Clients at terminals 2 and 3 operate the reminder at terminal 1).

images/d2rem02.png

Figure 3. Clients at terminals 2 and 3 operate the reminder at terminal 1.

Next let’s check the object’s life span. Stop both client sessions by typing exit at the irb prompt. Then restart the client in terminal 2.

  ​# [Terminal 2]​
  ​% irb --prompt simple -r drb/drb​
  ​>> r = DRbObject.new_with_uri('druby://localhost:12345')​
  ​>> r.to_a​
  ​=> [​
  ​[1, "13:00 Meeting"],​
  ​[3, "Return DVD on Saturday"],​
  ​[4, "15:00 Status report"]​
  ​]​

This is as we expected. The actual Reminder object exists on the server, so quitting and restarting the clients doesn’t have any impact on the data stored in it.

One of the benefits of using dRuby for your system is being able to share the state of a object across multiple processes. You could use dRuby as an alternative way to create persistence. For example, you could combine a short-running Common Gateway Interface (CGI) script and a long-running dRuby server when you write a web application.

To simplify the client operation, let’s write a ReminderCUI script. ReminderCUI is a character-based user interface that you can use via irb. This class has a list method to show all the items and a delete method to delete an item after confirmation.

reminder_cui0.rb
  class ReminderCUI​
  def initialize(reminder)​
  ​ @model = reminder​
  end
  ​​
  def list​
  ​ @model.to_a.each do |k, v|​
  ​ puts format_item(k, v)​
  end
  ​ nil​
  end
  ​​
  def add(str)​
  ​ @model.add(str)​
  end
  ​​
  def show(key)​
  ​ puts format_item(key, @model[key])​
  end
  ​​
  def delete(key)​
  ​ puts "[delete? (Y/n)]: #{@model[key]}"
  if /\s*n\s*/ =~ gets​
  ​ puts "canceled"
  return
  end
  ​ @model.delete(key)​
  ​ list​
  end
  ​​
  ​ private​
  def format_item(key, str)​
  ​ sprintf("%3d: %s\n", key, str)​
  end
  end

Let’s start ReminderCUI at terminal 3 and do some experimentation (see Figure 4, The ReminderCUI at terminal 3 operates the reminder at terminal 1).

images/d2rem03.png

Figure 4. The ReminderCUI at terminal 3 operates the reminder at terminal 1.
  ​# [Terminal 3]​
  ​% irb --prompt simple -I . -r reminder_cui0.rb -r drb/drb​
  ​>> there = DRbObject.new_with_uri('druby://localhost:12345')​
  ​>> r = ReminderCUI.new(there)​
  ​>> r.list​
  ​ 1: 13:00 Meeting​
  ​ 3: Return DVD on Saturday​
  ​ 4: 15:00 Status report​
  ​=> nil​
  ​>> r.add('Request Ruby Hacking Guide to library')​
  ​>> r.list​
  ​ 1: 13:00 Meeting​
  ​ 3: Return DVD on Saturday​
  ​ 4: 15:00 Status report​
  ​ 5: Request Ruby Hacking Guide to library​
  ​=> nil​
  ​>> r.delete(1)​
  ​ [delete? (Y/n)]: 13:00 Meeting​
  ​n # Type "n"​
  ​canceled​
  ​=> nil​
  ​>> r.delete(1)​
  ​ [delete? (Y/n)]: 13:00 Meeting​
  ​y # Type "y"​
  ​ 3: Return DVD on saturday​
  ​ 4: 15:00 status report​
  ​ 5: Request Ruby Hacking Guide to library​
  ​ => nil​

Using irb, you can create a multiclient program interface easily. We’ll add a web interface for this in Chapter 3, Integrating dRuby with eRuby, so hold on!

In this section, we created a simple distributed application using dRuby. By now, you should have a pretty good idea of how dRuby works. But before we leave this introductory chapter, let’s dig a little deeper into the URIs we’ve been using to access our dRuby servers.

The Hostname and Port Number

The hostname and port number components of the URI are optional on the call to DRb.start_service.

If the hostname is specified, the connection is associated with that network interface. If omitted, TCPServer will automatically assign the hostname.

If the port name is 0, the first available port will be automatically assigned.

If nil is passed instead of a URI, then it acts as if both the hostname and the port number were omitted.

Here are some examples:

You can always find the host and port that were used by start_service by calling the method DRb.uri. Let’s try this in irb.

  ​% irb -r drb/drb​
  ​irb(main):001:0> DRb.start_service('druby://localhost:12345'); DRb.uri​
  ​=> "druby://localhost:12345"​
  ​irb(main):002:0> DRb.start_service('druby://:12346'); DRb.uri​
  ​=> "druby://yourhost:12346"​
  ​irb(main):003:0> DRb.start_service('druby://localhost:0'); DRb.uri​
  ​=> "druby://localhost:52359"​
  ​irb(main):004:0> DRb.start_service('druby://:0'); DRb.uri​
  ​=> "druby://yourhost:52360"​
  ​irb(main):005:0> DRb.start_service(nil); DRb.uri​
  ​=> "druby://yourhost:52361"​
  ​irb(main):006:0> exit​

Now let’s try the “Hello, World” example without a URI. This time, use --prompt simple to simplify the irb prompt.

Start the server in terminal 1. Don’t pass a URI to it.

  ​# [Terminal 1]​
  ​% ruby puts00.rb​
  ​druby://yourhost:52369 # Auto generated URI​

Now start the client in terminal 2, using the URI displayed by the server.

  ​# [Terminal 2]​
  ​% irb --prompt simple -r drb/drb​
  ​>> uri = 'druby://yourhost:52369'​
  ​>> # Specify the same hostname and port number as in the terminal 1​
  ​>> there = DRbObject.new_with_uri(uri)​
  ​>> there.puts('Hello, World.')​

You should see “Hello, World.” pop up on terminal 1.

When we first tried the “Hello, World” app, we used localhost as the hostname, so we were able to run the clients on the same machine as the server. This time, we’ve used an externally accessible name, so you can try running a client on a separate machine that is networked with the server. Just specify the URI of the server, the same way you did when running the client locally.