one.world – Tutorial Part II


The RemoteCounter Application

In part II of the one.world tutorial, we will explore how to extend the Counter application to allow its value to be reset or synchronized to that of another counter.

RemoteCounter application windows

This part of the tutorial introduces

and illustrates

Note that the one.world tutorial uses three browser windows: this window for the tutorial itself, another for the source code, and another for class documentation.

You can find the entire source for this example in RemoteCounter.java.


Background

A few new concepts will be necessary in building the RemoteCounter application.

  • Operations. Operations are part of what we call the logic/operation pattern: code can be divided into computations that do not fail (logic) and asynchronous interactions that may fail (operations). Using an operation object makes it easier to manage asynchronous request-response interactions within this pattern by automatically performing timeouts and retries.

    When an event is sent through an operation, the operation sets a timer. Usually, the operation will soon receive a response to the event, and the response will be forwarded to the operation's continuation. If it does not receive a response to the event before the timer expires, the operation will send the event again, up to a certain number of retries. If the operation never receives a response to the event, it will send a TimeOutException to its continuation.

    Using an operation helps to ensure that an application will eventually receive a response to the request it sends, even if that response is only a notification that the operation has timed out.

  • Bindings and leases. In one.world, resources provided by the system, such as tuple stores and network channels, must be bound before they can be used. Every environment provides a request handler that accepts BindingRequests and attempts to return a binding to the desired resource.

    A BindingRequest has two important fields: a tuple that describes the desired resource, and the length of time during which the application intends to use the resource. The corresponding BindingResponse will contain an event handler that instantiates the desired resource, and also an event handler representing a lease. The lease controls how long the application may use the resource, and ensures that applications cannot accidentally fail to release resources. A LeaseEvent may be used to renew a lease or learn its current duration.

    Often, an application will want to use a resource for as long as the application is running. A LeaseMaintainer can be used to continually renew a lease until it is explicitly canceled or until the application stops.

  • Remote event passing. Similar to the use of asynchronous event passing instead of synchronous method invocation, remote event passing (REP) replaces remote method invocation in one.world. REP allows events to be sent to event handlers that live on different one.world nodes.

    To receive remote events, an event handler must bind a name that other nodes can use when sending events to it. Sending events through REP is easy: Simply wrap the event and its destination inside a RemoteEvent and pass the RemoteEvent to the imported environment request handler. With late binding, the destination is a NamedResource or a DiscoveredResource generated by the application. The event will be routed to any or all resources matching the named destination. With early binding, the destination is a RemoteReference obtained either from a request event or by resolving a NamedResource or a DiscoveredResource.


Using one.gui.Application

In part I of the tutorial, we used Skeletor to generate the structure of our main component, but we still had to fill in a lot of the details. Many of these details are the same for most applications, and get awfully repetitive.

So, we'll start by rewriting the Counter application using the one.gui.Application framework, which does most of the repetitive work for us and helps us to build the main window of a one.world GUI application. While we're at it, we'll also add widgets to the GUI that we will use for resetting the counter and for synchronizing the counter's value with that of another counter.

The one.gui.Application class does several things for us.

  • It extends Component and provides a component descriptor.
  • It exports a main handler that handles environment events, and it imports the environment's request handler.

  • It manages three application states: inactive, activating, and active. In the inactive state, it is not running. In the activating state, the application has received an activated, moved, cloned, or restored environment event and is acquiring the resources it requires to run. Finally, in the active state, an application is showing its main window and reacting to user input.

    The state transition from the inactive to the activating state is performed by the main exported event handler, which starts the resource acquisition process by invoking acquire(). The state transition from the activating to the active state is performed by the start() method. Finally, the state transition from the active to the inactive state is performed by the stop(boolean) method, which relies on release() to release an application's resources.

  • It helps us to manage GUI application windows. When the window is closed, the application is stopped.

To make RemoteCounter a subclass of Application, we need to override the createMainWindow() method, to create the main window, and the acquire() and release() methods, to manage the RemoteCounter's resources.

The createMainWindow() method should return a window for the RemoteCounter application. Writing this method is easy:

  /** Create the remote counter's main window. */
  public Application.Window createMainWindow() {
    return new Window(this);
  }

(Note that if our application didn't have a GUI, the createMainWindow() method would simply return null.)

The hard part is to define our own Window class to create, lay out, and manage the GUI components. The RemoteCounter window should have

  • a location source icon, which lets our application work with Emcee's drag and drop method (this customarily goes in the upper right corner of the window),
  • labels for displaying the counter name and value,
  • text entries for the remote host name, port, and counter name,
  • and buttons for synchronizing and resetting the counter.
The new window class's constructor does all the work of laying out the components. We make use of GuiUtilities.createSimpleGrid() to nicely lay out the text entries and their labels.

  /** Implementation of the remote counter's main window. */
  static final class Window extends Application.Window {
    
    /** A label containing the current count. */
    JLabel countLabel;

    /** The text field for the remote host name. */
    JTextField hostField;

    /** The text field for the remote port number. */
    JTextField portField;

    /** The text field for the remote counter name. */
    JTextField nameField;

    /**
     * Create a new main window.
     *
     * @param  counter  The remote counter component.
     */
    Window(final RemoteCounter counter) {
      super(counter, "Counter");

      // The location source icon.
      Environment env = counter.getEnvironment();
      JLabel      loc = GuiUtilities.createLocationSource(env.getId());
      GuiUtilities.addUserPopup(loc, env);

      // A label with the name of the hosting environment.
      JLabel heading = new JLabel(counter.format(env.getName()));
      heading.setAlignmentY(java.awt.Component.CENTER_ALIGNMENT);

      // A label with the count.
      countLabel = new JLabel(counter.getFormattedCount());
      countLabel.setHorizontalAlignment(JLabel.CENTER);
      countLabel.setAlignmentX(java.awt.Component.CENTER_ALIGNMENT);

      // The input fields for synchronizing with another counter.
      int etf = GuiUtilities.ENTRY_TEXT_FIELD;
      JComponent[] synchComponents =
          GuiUtilities.createSimpleGrid(
	      new String[] {"Host", "Port", "Name"},
	      new int[] {etf, etf, etf},
	      -1);
	      
      JComponent synchInputs = synchComponents[0];
      hostField = (JTextField) synchComponents[1];
      portField = (JTextField) synchComponents[2];
      nameField = (JTextField) synchComponents[3];

      hostField.setText(counter.host);
      portField.setText(new Integer(counter.port).toString());
      nameField.setText(counter.name);

      // The synchronize button.
      JButton synchButton = new JButton("Synchronize");
      synchButton.setAlignmentX(java.awt.Component.CENTER_ALIGNMENT);
      synchButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
	      synchronize();
	    }
          });
      
      // The reset button.
      JButton resetButton = new JButton("Reset count");
      resetButton.setAlignmentX(java.awt.Component.CENTER_ALIGNMENT);
      resetButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
	      resetCount();
	    }
          });

      // Pack the window.
      JPanel mainContent = new JPanel(new BorderLayout());
      mainContent.setBorder(BorderFactory.createEmptyBorder(3,3,3,3));

      Box locBox = new Box(BoxLayout.X_AXIS);
      locBox.add(heading);
      locBox.add(Box.createHorizontalGlue());
      locBox.add(loc);

      Box buttonBox = new Box(BoxLayout.X_AXIS);
      buttonBox.add(resetButton);
      buttonBox.add(Box.createHorizontalStrut(10));
      buttonBox.add(synchButton);

      Box box = new Box(BoxLayout.Y_AXIS);
      box.add(locBox);
      box.add(Box.createVerticalStrut(10));
      box.add(countLabel);
      box.add(Box.createVerticalStrut(20));
      box.add(synchInputs);
      box.add(Box.createVerticalStrut(10));
      box.add(buttonBox);

      mainContent.add(box);
      setContentPane(mainContent);

      if ((0 != counter.width) || (0 != counter.height)) {
        setSize(counter.width, counter.height);
      } else {
        pack();
      }
    }

    ...
  }

The Window class also needs methods for re-displaying the counter value when it changes each second. Since code that modifies Swing components needs to run in the Swing thread, we'll write two updateCount() methods. The first is thread-safe and can be called from our event handlers. It invokes the second method, which is not thread-safe and must be run in only the Swing event dispatch thread.

    /** Update the count label. This method is thread-safe. */
    void updateCount() {
      if (SwingUtilities.isEventDispatchThread()) {
        updateCount1();
      } else {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
              updateCount1();
            }
          });
      }
    }

    /** Update the count label. This method is not thread-safe. */
    void updateCount1() {
      countLabel.setText(((RemoteCounter)app).getFormattedCount());
      countLabel.repaint();
    }

To start out with, our acquire() and release() methods will be pretty simple. The only resource we need to manage is the timer notification for updating the counter, which we can manage using Timer's synchronous interface. We'll make the timer notification a protected member and define methods for setting and canceling it. We can use these methods when we are ready to acquire and release resources.

  /** Acquire the resources needed by the remote counter application. */
  public void acquire() {
    synchronized (lock) {
      if (INACTIVE == status) {
        return;
      }

      // Set up the timed count update notification.
      setUpdateTimer();
    }
  }

  /** Release the resources used by the remote counter application. */
  public void release() {
    cancelUpdateTimer();
  }

  /** Set up the timed count update notification. */
  public void setUpdateTimer() {
    synchronized (lock) {
      // Schedule the notification for every second, beginning one second
      // from now, if there isn't already an active timer notification.
      if (countUpdate == null) {
        countUpdate = timer.schedule(Timer.FIXED_RATE,
                                     SystemUtilities.currentTimeMillis()
		  		       + Duration.SECOND,
                                     Duration.SECOND,
                                     updateHandler,
                                     new DynamicTuple());

      }

  }

  /** Cancel the count update notifications. */
  public void cancelUpdateTimer() {
    synchronized (lock) {
      if (null != countUpdate) {
        countUpdate.cancel();
	countUpdate = null;
      }
    }
  }

What do we do when we get a DynamicTuple from the timer? Well, pretty much what we did back in part I of the tutorial: We update the counter value and display the new value in the window.

  /** Implementation of the count update handler. */
  final class UpdateHandler extends AbstractHandler {

    /** Handle the specified event. */
    protected boolean handle1(Event e) {
      if (! (e instanceof DynamicTuple)) {
        return false;
      }

      Window window;
      synchronized (lock) {

        // We only update the count if the counter application is actually
        // running.
        if (ACTIVE != status) {
          return true;
        }

        // Update the count value.
	count++;

        // Grab a reference to the main window.
        window = (Window)mainWindow;
      }

      // Update the count label.
      window.updateCount();

      // Done.
      return true;
    }
  }

There's one last small detail: We want to be able to reset the counter value. We'll add a method to the RemoteCounter class to let us set the counter to an arbitrary value (and update the label in the main window).

  /** 
   * Sets the count to the specified value.
   *
   * @param count  The new count value.
   */
  public void setCount(int newCount) {
    Window window;

    synchronized (lock) {

      // Don't do anything unless the application is active.
      if (ACTIVE != status) {
        return;
      }

      // Set the count value.
      count = newCount;

      // Reschedule the timer, so it will remain at the new value for a
      // full second.
      cancelUpdateTimer();
      setUpdateTimer();

      // Grab a reference to the main window.
      window = (Window)mainWindow;
    }

    // Update the count label in the main window.
    window.updateCount();
  }

Then the resetCount() method called when the user presses the "Reset count" button can simply call setCount() with a new counter value of 0.

    /** Reset the count to 0. */
    void resetCount() {
      ((RemoteCounter)app).setCount(0);
    }

There are two steps to making the "Synchronize" button work: First, the application must be able to receive and respond to synchronization requests from other remote counters. Second, the application must send a request to another remote counter when the user presses the "Synchronize" button, and it must act appropriately on the response.

Receiving remote events

Our application will receive requests from other RemoteCounter instances across the network. A request to synchronize will be a DynamicTuple with a field named "getCount". Because it is coming through the remote event passing system, it will be wrapped in a RemoteEvent.

Our application will respond to such a request with another DynamicTuple with a field named "count" that contains the current counter value. Note that we can't use this as the source of the DynamicTuple. Because this event may be sent to a different one.world node, we must use a SymbolicHandlersyncReference – instead. The dynamic tuple should be wrapped in a RemoteEvent with the request's source as its destination. We'll send the remote event to the environment's request handler, and it will attempt to deliver the remote event.

To keep the code clean, we'll write a separate event handler, nested in the RemoteCounter class, for handling these requests.

  /** Implementation of the synchronization request handler. */
  final class SyncRequestHandler extends AbstractHandler {

    /** Handle the specified event. */
    protected boolean handle1(Event e) {
      if (e instanceof RemoteEvent) {
        RemoteEvent re = (RemoteEvent)e;

	// If the nested event has a field named "getCount",
	// reply with the actual count.  We'll respond using
	// AbstractHandler's response method rather than an operation,
	// leaving it up to the requestor to retry if our message doesn't
	// get there.
	if (re.event.hasField("getCount")) {
	  DynamicTuple response = new DynamicTuple(syncReference, null);
	  response.set("count", new Integer(count));
	  respond(request, re.closure, re.event, response);
	  return true;
	}
      } 
      return false;
    }
  }

But how do these remote events get delivered to us in the first place? And how do we get the syncReference that we use for the source of the response? We need to export the synchronization request handler in the acquire() method by sending a BindingRequest to the environment's request handler.

The BindingRequest's descriptor should be a RemoteDescriptor, to indicate that we want to bind a name for remote event passing. The remote descriptor should contain the handler we want to export and a name for the handler. In this case, we need to export the synchronization request handler, and we'll use the name of the environment to export it, just for simplicity's sake. Also, we want this binding to last as long as the remote counter application is running, so we'll use Duration.FOREVER (i.e., as long as possible) for the requested lease duration.

    // We will export the synchronization request handler under this
    // environment's name on the local machine.
    RemoteDescriptor descriptor = 
        new RemoteDescriptor(syncRequestHandler,
	                     getEnvironment().getName());
 
    // This is the event to send to establish the binding.
    BindingRequest bindingRequest = 
        new BindingRequest(null, null, descriptor, Duration.FOREVER);

We'll use an operation to help make sure the binding request gets a response. If the response is a BindingResponse, we can keep the resource returned to us, which is a RemoteReference that refers to the synchronization request handler. We'll also create a LeaseMaintainer, which will help to maintain the binding as long as the application is running. Then we can start the application.

But, the binding might not succeed. For instance, someone else might already be using the name we want. If the binding doesn't succeed, we'll get an ExceptionalEvent. We might also get an event that we don't understand. Either way, we should display an error message to the user and stop the application. So, the code we add to the acquire() method ends up looking like this:

    // Using an operation, attempt to establish the binding.
    Operation op = 
        new Operation(timer, request, new AbstractHandler() {
	      protected boolean handle1(Event e) {
	      
	        if (e instanceof BindingResponse) {
		  // The binding succeeded.
		  BindingResponse response = (BindingResponse)e;

		  // Hang on to the resource.
		  syncReference = (RemoteReference)response.resource;

		  // Maintain the binding.
		  leaseMaintainer = 
		      new LeaseMaintainer(response.lease,
		                          response.duration,
					  syncRequestHandler,
					  null, 
					  timer);
					 
		  // Start the application.
                  start();

		  // All done.
		  return true;
		} 

		// If we didn't get a binding response, the binding
		// failed.
		Throwable x;
		if (e instanceof ExceptionalEvent) {
		  x = ((ExceptionalEvent)e).x;
		} else {
		  x = new UnknownEventException(e.getClass().getName());
		}
	        JOptionPane.showMessageDialog(mainWindow,
		      "Unable to start RemoteCounter:\n" + x,
	              "RemoteCounter Startup Error",
		      JOptionPane.ERROR_MESSAGE);
		stop(true);
		return true;
	      }
            });
	    
    // Start the operation.
    op.handle(bindingRequest);

We now have another resource to manage (the REP binding), so the release() method needs to do some more work. In addition to canceling the timer, it needs to cancel the lease maintainer for the REP binding:

    if (leaseMaintainer != null) {
      leaseMaintainer.cancel();
      leaseMaintainer = null;
   }

The RemoteCounter can now receive and respond to requests from other RemoteCounter instances. Finally, we can write the code that determines what happens when the user presses the "Synchronize" button.

Sending remote events

When the user presses the "Synchronize" button, we'll store the text from the host, port, and name text fields. This not only lets us use them to get the value of the other counter, but also lets us display this text to the user later if they decide to checkpoint and restore the application. Then, we'll call a method that does the actual work of synchronization.

    /** 
     * Synchronize this counter with the user-specified remote counter.
     */
    void synchronize() {
      RemoteCounter counter = (RemoteCounter)app;
      try {
        counter.setRemoteCounter(hostField.getText(),
                                 Integer.parseInt(portField.getText()),
	                         nameField.getText());
        counter.synchronize();
      } catch (NumberFormatException x) {
        // Show an error message to the user.
	JOptionPane.showMessageDialog(this,
	              "Please enter an integer for the remote port number",
	              "RemoteCounter Runtime Error",
		      JOptionPane.ERROR_MESSAGE);
      }
    }

We'll do the actual work of synchronizing with the other counter using an operation. Since users are impatient, we'll use an operation with a short timeout (5 seconds) and only one retry.

We'll need to export the operation just like we did the SyncRequestHandler, so that it can receive a response to the request that it sends. We can export it anonymously -- that is, without providing a name -- since we only want the operation to get responses to the events that it sends. We can use a short lease and no lease maintainer, since this will be a quick interaction.

Once the operation is exported, we'll use it to send a DynamicTuple with the remote reference from the BindingResponse as the source, nested in a RemoteEvent with a NamedResource as its destination. If we get a DynamicTuple back in response, we'll set the counter value to the value of its "count" field. If the operation times out, or if another error occurs such as not being able to connect to the remote host or find a resource with the specified name, we'll display an error message to the user. (Note that we didn't use an operation or handle any errors when sending the response in syncRequestHandler -- it's simpler if we assume the requestor will try again.)

Here's the full synchronize() method:

  public void synchronize() {

    // Disable the main window.
    mainWindow.setEnabled(false);

    // Create an operation with a short timeout and only one retry. 
    // (Users are impatient.)
    final Operation op = new Operation(1, 5*Duration.SECOND, 
                                       timer, request, null);
    
    // The operation will need to start by exporting its response handler
    // as an anonymous remote resource, to obtain a remote reference.
    Event bindingRequest = 
        new BindingRequest(null, null, 
	                   new RemoteDescriptor(op.getResponseHandler()),
			   Duration.MINUTE);
  
    // Set the operation's continuation.
    op.continuation = new AbstractHandler() {
          protected boolean handle1(Event e) {
	    if (e instanceof BindingResponse) {
	      BindingResponse response = (BindingResponse)e;

	      // A remote reference for this operation.
	      RemoteReference ref = 
	          (RemoteReference)response.resource;

              // The remote counter resource.
	      NamedResource remote =
	          new NamedResource(host, port, name);
 
              // The event we will send to the remote counter.
	      DynamicTuple dt = new DynamicTuple(ref, null);
	      dt.set("getCount", Boolean.TRUE);
              
	      // Send the value request.
	      op.handle(new RemoteEvent(null, null, remote, dt));

	      // Done for now.
	      return true;
	                   
	    } else if (e instanceof RemoteEvent) {
	      RemoteEvent re = (RemoteEvent)e;

	      // If the nested event has an integer field named "count",
	      // that is the new count value.
	      Object o = re.event.get("count");
	      if (o instanceof Integer) {
	        setCount(((Integer)o).intValue());

                // Re-enable the main window.
                mainWindow.setEnabled(true);
		return true;
	      }

	    } else if (e instanceof ExceptionalEvent) {
	      ExceptionalEvent xe = (ExceptionalEvent)e;
	      JOptionPane.showMessageDialog(mainWindow,
                  "Unable to synchronize with " + host + ":" + port + "/"
		      + name + ":\n" + xe.x,
		  "RemoteCounter Runtime Error",
		   JOptionPane.ERROR_MESSAGE);
              // Re-enable the main window.
              mainWindow.setEnabled(true);
	      return true;
	    }
	    return false;
	  }
      };

    // Start the operation.
    op.handle(bindingRequest);
  }

Further information

RemoteCounter is part of the one.world distribution and lives in the one.toys package. Go ahead and play with it. (It's more interesting if you can run at least two Remote-Counters on two different machines and synchronize them with each other.) Remember that the name of the remote counter is the name of the environment that it runs in.

In addition to the point-to-point remote event passing we have used here, a discovery service allows resources on a local network to be described with a tuple and discovered using a query, independent of which one.world node the resource lives on. Events sent through the discovery service may be either anycast or multicast. There is very little difference between the interface for point-to-point remote event passing and that for discovery. See one.world.rep.RemoteDescriptor, one.world.rep.RemoteEvent, and one.world.rep.DiscoveredResource to learn about the differences.

In the one.world source distribution, one.toys.RemoteSender and one.toys.RemoteReceiver provide a simple example of remote event passing, both point-to-point and through discovery, while one.radio.Chat is a realistic application that makes extensive use of discovery.