Notes

Powered by The LE Company

Sunday, August 29, 2010

Threads and Callbacks in RMI

(from: http://www.cs.swan.ac.uk/~csneal/InternetComputing/ThreadCallBack.html)

7.1. Background

So far, all the RMI examples you have seen have been very straightforward - the client needs to invoke some service on a server, and is prepared to wait until the service has finished before proceeding. But what if the service will take some time, and the client wishes to get on with something else while it waits? Or if the activities of the server are to some degree independent of main function of the client? The first situation is often the case when dealing with GUI code - it is not considered good practice to freeze up a GUI while it performs some slow operation.
Also, suppose there are multiple clients - what happens if more than one tries to access the service at the same time? We will deal with this issue first, as it is the simplest.

7.2. RMI Threads

We will start off with an RMI version of the Fibonacci server - if you recall the socket version worked quite happily with multiple clients. First we need an interface:

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Fib extends Remote {

    int getNextFib()
        throws RemoteException;

}
Fairly obviously, getNextFib will return the next Fibonacci number. Now we need some server code:

import java.rmi.*;  
import java.rmi.server.*;

public class FibServer extends UnicastRemoteObject
                          implements Fib {
    
    private int F1 ;
    private int F2 ;        
    
    public FibServer() throws RemoteException {
        F1 = 1;
        F2 = 2;
    }       
    
    public int getNextFib()
            throws RemoteException {
            
    int temp = F1;
    
    
    F1 = F2;
    F2 = temp + F2;
    
    return temp;
    }

    
    public static void main (String args[]) {
    
        try {
            FibServer server = new FibServer();
            Naming.rebind("FIB", server);
        
        }
        catch (java.net.MalformedURLException e) {
            System.out.println("Malformed URL " + e.toString());
        }
        catch (RemoteException e) {
            System.out.println("Communication error " + e.toString());
        }
    }
}
Notice that unlike the socket version, this is not explicitly threaded. Finally we need some client code:

import java.rmi.*;
import java.lang.*;

public class FibClient {

    public static void main (String args[]) {
    
        Fib serverObject;
        Remote RemoteObject;
    
        /* Should put in security manager */
        try {
            /* See if the object is there... */
            String name = "rmi://localhost/FIB";
            RemoteObject = Naming.lookup(name);

            serverObject = (Fib)RemoteObject;
            for (int i = 0; i < 100; i++) {
                System.out.println(serverObject.getNextFib() + "\n");
            }
        }
        catch (Exception e) {
            System.out.println("Error in invoking object method " +
                e.toString() + e.getMessage());
                e.printStackTrace();
        }
    }
}
To avoid cluttering up the code, we have left out the security manager.
To make things easier you can download the interface, server and client.

7.2.1. Running...

Compile the application in the usual way - locally on one machine is sufficient - and try connecting to the server with two (or more) clients. First impressions... Great! It works! However... Look more closely. You will notice that the server is only generating one set of Fibonacci numbers, which are being shared between the clients. This is because we have only created one server object, and the RMI runtime is simply scheduling multiple calls to the same object. You can think of the server starting up multiple threads, one for each client, and then each of those threads calls the (single) server object. The actual situation is a bit more complex than this, but that need not concern us here.
In the case of the Fibonacci server, this is not the behaviour we want or expect - however, in most actual applications, it is. We typically want to provide parallel, distributed access to a common data set. Of course, the usual problems of parallel access to a common data set - especially when one or more clients can alter the data - still apply. But the good news is that in most circumstances, multiple RMI clients can work without any explicit threading code needing to be written on the server (though you may still want to thread the server for, say, performance reasons). However, even though we have not included it in the example above, in general the same issues relating to controlling access to shared data exist as with our socket based example - and we'll consider this in more detail in the next chapter.

7.3. Callbacks

Most people learn to program using a sequential model - they write some program that reads some input, and it waits until that input arrives before proceeding. Much real code does not work like this - it is event driven. Such code will some how set up bits of code that get called when certain events occur - in Java this commonly happens with GUI code and we talk about registering event handlers, or event listeners. A generalization of this idea is the callback - code that is called in application (or client) A by application (or server) B as a result of something that A did previously.
The following simple (and silly) example illustrates the problem. We have the following RMI interface:

import java.rmi.*;

public interface Fruit1 extends Remote {
    public String nextFruit() throws RemoteException ;
}
This is implemented by the following server code:

import java.rmi.*;
import java.rmi.server.*;

public class Main extends UnicastRemoteObject implements Fruit1 {
    
    private String fruity[]={"apple","orange","strawberry","mango"};
    int i = 3;
    
    public Main() throws RemoteException {
    }
    
    public String nextFruit() throws RemoteException {
        try {
            i++;
            if (i == 4) {
                i = 0;
            }
            Thread.sleep(3000);
        }
        catch(Exception e){
            System.out.println("Oh no!!!");
        };
        return fruity[i];
    }

    public static void main(String[] args) {
            try {
            
            Main server = new Main();
            Naming.rebind("FRUIT", server);
        
        }
        catch (java.net.MalformedURLException e) {
            System.out.println("Malformed URL for MessageServer name " + e.toString());
        }

        catch (RemoteException e) {
            System.out.println("Communication error " + e.toString());
        }
    }
    
}
This code just simply returns the next string in an array - but it takes a long time to do it because we have inserted a delay. In this case, the delay is artifical - but it may well be the case that a server takes some time to work. Here is the client code, with the GUI initialization and some other stuff omitted:


import java.rmi.*;

public class FruitClient1 extends javax.swing.JFrame {
    
    private javax.swing.JLabel activityLabel;
    private javax.swing.JTextField fruitLabel;
    private javax.swing.JButton getFruit;
    private Fruit1 fruit;
    
    public FruitClient1() {
        initComponents();
        LabelChanger lb = new LabelChanger(activityLabel);
        lb.start();
        try {
            Remote remoteObject = Naming.lookup("FRUIT");

            fruit = (Fruit1)remoteObject ;
        }
        catch(Exception e){
            System.out.println("Oh no!!!");
        };
        
    }
    

    private void initComponents() {
        /* GUI initialization omitted */
    }

    private void getFruitActionPerformed(java.awt.event.ActionEvent evt) {
        try {
            fruitLabel.setText(fruit.nextFruit());
        }
        catch(Exception e){
            System.out.println("Oh no!!!");
        };
    }
    
    public static void main(String args[]) {
        /* code to run application omitted */
    }

}

class LabelChanger extends Thread {
    
    private javax.swing.JLabel label;
    
    public LabelChanger(javax.swing.JLabel lb){
        label = lb;
    }
    
    public void run() {
        while (true){
            try {
                label.setText("Hello There!");
                Thread.sleep(1000);
                label.setText(" ");
                Thread.sleep(1000);
            }
        catch(Exception e){
            System.out.println("Oh no!!!");
        };
        }
    }
}
This client consists of three GUI components - a label which just alternates between being blank and saying 'Hello There!' every second. The only reason for this is to show that the GUI is still active (or not). The other components are (a) a button, which when clicked calls the server and writes the corresponding text in (b) a text field.
You can download the interface, server and client.
Compile and run the application - it works, but the GUI freezes when the button is clicked until the string is returned. This is not good.

7.3.1. Aside: Threads and Swing

Note that there is an important issue here - we have created a Swing user interface, and then used it with threaded code. Strictly speaking, Swing is not thread safe - that is, things can go badly wrong if we do this, and we should really follow the appropriate procedures to produce thread safe code. In practice, because we are not writing to any GUI component from more than one thread, there is no practical problem.

7.3.2. A Simple Solution: Polling

The obvous thing we need to do is somehow get the GUI to start the operation on the server, but not wait for it to finish. Instead, we somehow want the GUI to be notified when the server has finished. The problem with our current model is that we have a clear cut distinction between server and client - the client asks the server to do things, not the other way around. Thinking about this, a simple solution is a technique called polling - where the client periodically asks the server 'have you finished yet?'. When the server says yes, the client calls another method to retrieve the new value. Here is the new interface.

import java.rmi.*;

public interface Fruit2 extends Remote {
    public void nextFruit() throws RemoteException ;
    
    public boolean doneYet() throws RemoteException;
    
    public String getFruit() throws RemoteException;
}
Here is the new server, which now must start a separate thread to compute its result:

import java.rmi.*;
import java.rmi.server.*;
import java.lang.*;
import java.util.*;


public class Main extends UnicastRemoteObject implements Fruit2 {
    
    DoFruit df = new DoFruit(); // A bit flaky...
    
    public Main() throws RemoteException {
    }
    
    public void nextFruit() throws RemoteException {
        df = new DoFruit();
        df.start();
    }
    
    public boolean doneYet() throws RemoteException {
        return df.doneYet();
    }
    
    public String getFruit() throws RemoteException {
        return df.getFruit();
    }

    public static void main(String[] args) {
            try {
            
            Main server = new Main();

            Naming.rebind("FRUIT", server);
        
        }
        catch (java.net.MalformedURLException e) {
            System.out.println("Malformed URL for MessageServer name " + e.toString());
        }
        catch (RemoteException e) {
            System.out.println("Communication error " + e.toString());
        }
    }
    
}

class DoFruit extends Thread {
    private String fruity[]={"apple","orange","strawberry","mango"};
    private static int i = 3;
    private boolean done = false;
    
   public void run()  {
        try {
            done = false;
            i++;
            if (i == 4) {
                i = 0;
            }
            Thread.sleep(3000);
            done = true;
        }
        catch(Exception e){
            System.out.println("Oh no!!!");
        };
    }
    
    public String getFruit() {
        done = false;
        return fruity[i];
    }
    
    public boolean doneYet() {
        return done;
    }
}
The two new methods respectively check the status of the thread, and return the new string value if the thread has finished computing it. We use the boolean done to indicate if the computation has finished or not. Here is the new client code, again with omissions, that checks if the server has finished 10 times a second. Notice we need another thread to control the content of the textfield. Note we initialize df initially with a `dummy' thread that we never start - if we don't we get a nullPointerException on every call to doneYet until we actually call nextFruit - code here could be better.

import java.rmi.*;

public class FruitClient2 extends javax.swing.JFrame {
   
    private javax.swing.JLabel activityLabel;
    private javax.swing.JTextField fruitLabel;
    private javax.swing.JButton getFruit;
    private Fruit2 fruit;
    private FruitChecker testFruit;
     
    public FruitClient2() {
        initComponents();
        LabelChanger lb = new LabelChanger(activityLabel);
        lb.start();
        try {
            Remote remoteObject = Naming.lookup("FRUIT");

            fruit = (Fruit2)remoteObject ;
        }
        catch (Exception e){}
        testFruit = new FruitChecker(fruitLabel, fruit);
        testFruit.start();
        
    }
    
    private void initComponents() {
        /* GUI initialization */
    }

    private void getFruitActionPerformed(java.awt.event.ActionEvent evt) {
        try {
            fruit.nextFruit();
        }
        catch(Exception e){
            System.out.println("Oh no!!!");
        };
    }
    

    public static void main(String args[]) {
        /* run the client */
    }

}

class LabelChanger extends Thread {
    
    //Same as before
}

class FruitChecker extends Thread {

    private javax.swing.JTextField textfield;
    private Fruit2 remoteFruit;

    public FruitChecker(javax.swing.JTextField tf, Fruit2 f) {
        textfield = tf;
        remoteFruit = f;
    }
    
    public void run() {
        while (true) {
            try {
                Thread.sleep(100) ;
                if (remoteFruit.doneYet()) {
                    textfield.setText(remoteFruit.getFruit());
                }
            }
        catch(Exception e){
            System.out.println("Oh no!!!");
        };
        }
    }
}
You can download the interface, server and client.
Compile and run the application - it works, and the GUI remains responsive (a bit pointless in this case, but you can imagine applications where other, local, operations could proceed while the server was busy). The only real problem with polling that - potentially - it is resource expensive. In our case, this is not really an issue - but it is possible to consider cases where this would be a problem.

7.3.3. A Better Solution - Callbacks

It would be better, in general, if we could somehow get rid of the step in which the client asks if the server has finished, and just get the server to invoke some method on the client. The way to do this is to make the client an RMI server as well - by extention, the main RMI server also becomes an RMI client. This now seems to get complicated - at first thought, you need to run the RMI registry and a web server on the client as well as the server. This is only partly true - you need the web server, but not the registry. The reason you don't need the RMI registry is because its function is to enable clients to locate services. This is not necessary in this case because the calling client already knows where the service is (it's running it) and so it can simply pass a reference to its own remote callback service as a parameter when it first calls the server. This solution needs two more files: as well as the interface, server and client, we also need a new interface for the callback server and a new class to implement the callback server. Here is the main server interface - notice the polling method is now gone, but that the nextFruit method now takes a parameter of class (actually interface) Notify. This will be a reference to the callback server on the client.

import java.rmi.*;

public interface Fruit3 extends Remote {
    public void nextFruit(Notify n) throws RemoteException ;
    
    public String getFruit() throws RemoteException;
}
Here is the new callback interface:

import java.rmi.*;

public interface Notify extends Remote {
    public void doneIt() throws RemoteException;
}
Here is the server code:

import java.rmi.*;
import java.rmi.server.*;
import java.lang.*;
import java.util.*;


public class Main extends UnicastRemoteObject implements Fruit3 {
    
    DoFruit df;
    
    public Main() throws RemoteException {
    }
    
    public void nextFruit(Notify n) throws RemoteException {
        df = new DoFruit(n);
        df.start();
    }
    
    public String getFruit() throws RemoteException {
        return df.getFruit();
    }

    public static void main(String[] args) {
            try {
            
            Main server = new Main();

            Naming.rebind("FRUIT", server);
        
        }
        catch (java.net.MalformedURLException e) {
            System.out.println("Malformed URL for MessageServer name " + e.toString());
        }
        catch (RemoteException e) {
            System.out.println("Communication error " + e.toString());
        }
    }
    
}

class DoFruit extends Thread {
    private String fruity[]={"apple","orange","strawberry","mango"};
    private static int i = 3;
    private Notify notify;
    
    public DoFruit(Notify n) {
        notify = n;
    }
    
    public String getFruit() {
        return fruity[i];
    }
    
    public void run()  {
        try {
            i++;
            if (i == 4) {
                i = 0;
            }
            Thread.sleep(3000);
            notify.doneIt();
        }
        catch(Exception e){
            System.out.println("Oh no!!!");
        };
    }
}
We don't need the boolean done anymore - we simply call the callback method doneIt to tell the client we have finished.
Here is the client code:


import java.rmi.*;
import java.rmi.server.*;

public class FruitClient3 extends javax.swing.JFrame {
   
    private javax.swing.JLabel activityLabel;
    private javax.swing.JTextField fruitLabel;
    private javax.swing.JButton getFruit;
    private Fruit3 fruit;
    private FruitChecker notifyFruit;
     
    public FruitClient3() {
        initComponents();
        LabelChanger lb = new LabelChanger(activityLabel);
        lb.start();
        try {
            Remote remoteObject = Naming.lookup("FRUIT");

            fruit = (Fruit3)remoteObject ;
            notifyFruit = new FruitChecker(fruitLabel, fruit);
        }
        catch(Exception e){
            System.out.println("Oh no!!!");
        };      
    }
    
    private void initComponents() {
        /* GUI initialization */
    }

    private void getFruitActionPerformed(java.awt.event.ActionEvent evt) {
        try {
            fruit.nextFruit(notifyFruit);
        }
        catch(Exception e){
            System.out.println("Oh no!!!");
        };
    }
    

    public static void main(String args[]) {
        /* start GUI */
    }

}

class LabelChanger extends Thread {
    
    //Same as before
}
This is very similar to the previous version using polling, but we do not need the extra thread. However, we do need this new class to implement the callback, and actually set the text field:

import java.rmi.*;
import java.rmi.server.*;

public class FruitChecker extends UnicastRemoteObject implements Notify {

    private javax.swing.JTextField textfield;
    private Fruit3 remoteFruit;

    public FruitChecker(javax.swing.JTextField tf, Fruit3 f) throws RemoteException {
            textfield = tf;
            remoteFruit = f;
    }
    
    public void doneIt() {
        try {
            textfield.setText(remoteFruit.getFruit());
        }
        catch(Exception e){
            System.out.println("Oh no!!!");
        }
    }
}
You can download the interface, server, client, callback interface and callback server.
Compile and run the application. Which is best? In general, callbacks are a 'nicer' solution: they are bit more complex, and need a bit more code, but consume less resources in practice. However, the need to run a web server will rule out this solution in some cases.

7.3.4. Fourth Version

It was a bit late at night when I wrote version 3 - and there is a simplified version 4 available. I won't discuss it in the notes but you can get the files: interface, server, client, callback interface and callback server. The obvious simplification is made - the string to be displayed is passed as a parameter to doneIt.

1 comment: