Distributed Computing
Total Page:16
File Type:pdf, Size:1020Kb
http://www.javacats.com/US/articles/#Jini1
Jini Part 1
Benoît Marchal Jini, Part 1 This article is the first article in a new series on Jini. Jini, as you probably have heard by now, is a comprehensive object-oriented framework to build distributed applications. Jini extends Java paradigm, "write once run anywhere" into "write once run from anywhere." Jini extends the familiar RMI (Remote Method Invocation) protocol with look-up, leasing and transaction services. In this new series, we will examine the Jini framework in some details. This first article explains how to download and install Jini and shows how to write a simple application, using Jini. Distributed Computing Using Jini, it is possible to write powerful distributed applications. Jini supports the development of a "federation" of devices and software components, working together in a single distributed computing space. These devices and software components provide "services" that are available to the federation. These services can be hardware facilities (printing, scanning) or software one (heavy-duty computing or user-interfaces onto other environment). Jini builds on Java, in fact Jini is a Java-specific technology that requires Java platforms. Why? Because the devices and software components in the federation will exchange objects between each other. To support this, they need a mechanism to distribute compiled applications across heterogeneous platforms in a secure way -- exactly what Java offers. Sun insists that the federation are of a very dynamic nature. Services are dynamically added and removed. Furthermore, applications can discover services at run-time, they need very little prior knowledge. Jini uses RMI as its low-level communication protocol. If you already know how to write distributed applications with RMI, you will have an head-start with Jini. However don't panic if you are new to distributed computing, the concepts are easy to understand. Jini dramatically extends RMI with new services such as transaction management, remote event dispatching, leasing of objects and dynamic discovery and lookup. Again these services won't be exactly new to you if you have some experience of distributed computing, some of them looks a lot like CORBA services. However, unlike CORBA, Jini is a Java-only solution. This makes it much simpler but may be restrictive for some applications. Central to the working of Jini is the Discovery and Join protocols. As the name implies, the discovery protocol makes it possible for a Java Virtual Machine to discover a federation at run-time. It can then join the federation. One can see this as an extension of the familiar RMI registry. Getting Started In the first article in this new series, I will walk you through downloading and installing Jini. I will also show you how to write a simple Jini application. This application won't make much use of the advanced services such as remote event or transaction. Instead, I will concentrate on building a very simple distributed application. Compiling and running the first Jini application can be something of a challenge. Indeed because Jini is an architecture, you will have to start install and start many elements to get started. Error messages are not always easy to read and, to make things worse, Jini 1.0 ships with no real example. I hope I have packed enough guidance in this article to help you overcome all these limitations. Before you can take the Jini road, make sure you have downloaded and installed Java 2 (also known as the JDK 1.2). Jini uses some extensions to RMI that ships with Java 2 and Jini simply won't work with older version of the JDK. If you have not done so already, go to http://java.sun.com/ and download the latest version of the JDK. Next download Jini 1.0 itself. Jini ships as a ZIP file that you to uncompress and install on your system. It comes complete with documentation, source code and libraries but, alas, no simple example, such as an "Hello World" application. We will now see how to write a simple application, SavingsCalculator. SavingsCalculator shows you how to get rich by computing how much money you can earn through saving. Given an initial amount of money, your yearly savings, the interest rates you obtain, the service will compute how much money you will have saved after a given number of years. See how much one percent difference makes over several years, you will be surprised!
The Interface The first step is to write an interface for our new service. The interface is a contract between the client and the server, it defines which method the server implements. The server will export an object that implements the interface and the client can remotely make calls to the object. Listing 1 is the SavingsCalculator interface. import java.rmi.*; public interface SavingsCalculator extends Remote { public double calculate(double initial, double yearly, double interest, int duration) throws RemoteException; } Listing 1: SavingsCalculator interface As you can see, the interface is very simple and defines a single method. Like every RMI interface, it extends Remote and its method can throw a RemoteException. This is how the environment recognizes a remote interface. The Server The next step is to write the server. To turn it into a remote object, the server class extends UnicastRemoteObject. It also implements the SavingsCalculator interface. The class also implements the ServiceIDListener interface. ServiceIDListener implements only one method, serviceIDNotify() but is needed for Jini.
The interesting bit in listing 2 is the main() method. A typical RMI application uses the RMI registry to export its object to the world (through the Naming class) but this server uses the Jini lookup mechanism.
Firstly the server installs an RMISecurityManager. This manager controls code that Jini would download dynamically. Lookup is more flexible than the RMI registry, if only because it associates attributes with the remote objects. These attributes can more completely describe the object. The attributes are quite flexible and can even include status information (e.g. the printer has no paper left). As we will see, clients query the lookup based on these attributes. This server uses no less than three attributes to identify itself: a name, a comment and some information. Note that it could use more, it could even have multiple names, for example the name in English and the name in Japanese.
SavingsCalculatorImpl uses double to compute the interest. In a real financial application, this may result in rounding errors and you would want to use fixed precision numbers.
When the object is ready, it is exported through a JoinManager. import java.io.*; import java.rmi.*; import java.rmi.server.*; import com.sun.jini.lease.*; import net.jini.core.entry.*; import com.sun.jini.lookup.*; import net.jini.core.lookup.*; import net.jini.lookup.entry.*; public class SavingsCalculatorImpl extends UnicastRemoteObject implements SavingsCalculator, ServiceIDListener { public SavingsCalculatorImpl() throws RemoteException { super(); }
public void serviceIDNotify(ServiceID sid) { }
public double calculate(double initial, double yearly, double interest, int duration) throws RemoteException { interest = 1.0 + (interest / 100); double ending = initial; for(int k = 0;k < duration;k++) ending = yearly + (interest * ending); return ending; }
public static void main(String[] args) throws RemoteException, IOException { System.setSecurityManager(new RMISecurityManager()); SavingsCalculatorImpl sc = new SavingsCalculatorImpl(); Entry[] entries = { new Name("SavingsCalculator"), new ServiceInfo("SavingsCalculator", "Pineapplesoft", "Digital Cat", "1.0", "",""), new Comment("This service demonstrates how to get started with Jini.") }; new JoinManager(sc, entries, sc, new LeaseRenewalManager()); System.out.println("Server ready"); } } Listing 2: the server Thanks to RMI, it is very easy to write client/server applications. As you can see, RMI almost completely hide the network communication. Having to inherit from UnicastRemoteObject and throwing RemoteException is a small price for a distributed application. The Client Finally we write the client, see listing 3. Again the client starts by installing an RMISecurityManager. Next it acquires a reference to a locator. Through the locator, and its associated registrar object, it can lookup the server. You will remember the server has several attributed attached to it, the client only need to specify one of them. This gives tremendous flexibility: a server can register itself under a multitude of attributes to suits the needs of different clients. In this example, the client queries on the name of the server. The lookup() method returns a reference to the remote object. The client then call the calculate() method on the remote object. import java.io.*; import java.rmi.*; import net.jini.core.entry.*; import net.jini.core.lookup.*; import net.jini.lookup.entry.*; import net.jini.core.discovery.*; public class SavingsCalculatorClient { protected static void printHelp() { System.out.println("usage is: SavingsCalculatorClient initial monthly interest duration"); } public static void main(String[] args) throws RemoteException, IOException, ClassNotFoundException { System.setSecurityManager(new RMISecurityManager()); try { LookupLocator lookup = new LookupLocator("jini://localhost"); ServiceRegistrar registrar = lookup.getRegistrar(); Entry[] entries = { new Name("SavingsCalculator") }; SavingsCalculator savings = (SavingsCalculator)registrar.lookup(new ServiceTemplate(null,null,entries));
System.out.println(savings.calculate(Double.valueOf(args[0]).doubleValu e(),
Double.valueOf(args[1]).doubleValue(),
Double.valueOf(args[2]).doubleValue(),
Integer.parseInt(args[3]))); } catch(NumberFormatException e) { printHelp(); } catch(ArrayIndexOutOfBoundsException e) { printHelp(); } } } Listing 3: the client Although this looks a lot like normal object-oriented code, it is a distributed client/server application: the calculation is done by the server on behalf of the client. Compiling and Running Compiling and running the first Jini project is some sort of a challenge. There are many things to install and it is easy to forget something. The sanction however is immediate: the software does not run. The listing in this section are Windows batch files. Paths are specific to my system, you will probably have to adapt them for your environment. Also, on Unix systems, you will want to replace them with shell scripts to perform similar functions. Also most of these applications (the web server, rmid and SavingsCalculator) are servers. They won't terminate so you have to open several command-line window or use the start command (& on a Unix system). Again note that Jini requires Java 2. It will not run on the JDK 1.1 or the JDK 1.0. Compiling The Jini run-time is implemented in several JAR files. Compiling is not very difficult if you know which files you have to include in the CLASSPATH. Listing 4 compiles the server and the client. It also call the RMI compiler on the server. This compiler will generate so-called stub files. Stub files implements the magic of RMI: it is these files that implements the RMI protocol. javac -classpath jini-core.jar;sun-util.jar;jini-ext.jar;. SavingsCalculatorImpl.java javac -classpath jini-core.jar;sun-util.jar;jini-ext.jar;. SavingsCalculatorClient.java rmic -classpath jre\lib\rt.jar;jini-core.jar;sun-util.jar;jini- ext.jar;. SavingsCalculatorImpl Listing 4: compiling the server and the client Running If compiling was easy, running the application involves several steps: you must start the RMI activation daemon and the Jini lookup service before your application. Don't panic, this sections shows you how to do it, step by step. Firstly it is important to publish the stub classes on a web server. You can use any web server, such as Netscape, Apache or IIS, but if you don't have a web server, Jini ships with one. In this article, I will use Jini web server. You will also need to publish the reggie-dl.jar file which comes with the Jini environment. Simply create a separate directory (I called it web) and copy all the files. Now you can start the Jini web server as in listing 5. copy SavingsCalculatorImpl_Skel.class web copy SavingsCalculatorImpl_Stub.class web copy reggie-dl.jar web java -jar tools.jar -port 8080 -dir c:\web -verbose Listing 5: starting the web server Jini and RMI use the web server to distribute stub classes to the clients. This is the exciting bit with Java: if the client doesn't have all the code needed, it can be downloaded. Next you will have to start the RMI activation daemon, rmid, as illustrated in listing 6. The activation daemon creates several log files. When you restart the daemon, you absolutely want to delete old logs otherwise the daemon won't work properly. echo y | del log rd log rmid Listing 6: running the RMI activation daemon Finally you start reggie, the default implementation of the lookup service, as shown in listing 7. Reggie also creates a log directory that you need to clean up before restarting it. echo y | del c:\reggie_log rd c:\reggie_log java -jar -Djava.security.policy=policy.all reggie.jar http://localhost:8080/reggie-dl.jar policy.all c:\reggie_log public Reggie is slow to start, up to one minute. Also, unlike the web server or rmid, it does not need to run forever: it will install itself and terminates. Wait until reggie has installed itself before before running your application. Listing 7: starting reggie
Reggie takes a policy file on the command line. Remember the RMISecurityManager? It enforces security controls on the classes you download from the web server. RMISecurityManager uses the policy file to decide which classes can run. For testing purposes, the simplest solution is to give unlimited rights to all classes. Listing 8 is the policy.all file that does just that. grant { permission java.security.AllPermission "", ""; }; Listing 8: policy.all After all this work, you are ready for the real fun: running your first Jini application. First start the SavingsCalculator server, as shown in listing 9. Listing 10 invokes the client to calculate how much money you have if you save $1000 at an interest rate of 4% during 10 years. It also assumes that you deposit an extra $100 every year. java -classpath jini-core.jar;sun-util.jar;jini-ext.jar;. -Djava.security.policy=policy.all -Djava.rmi.server.codebase=http://localhost:8080/ SavingsCalculatorImpl Listing 9: starting the server java -classpath jini-core.jar;sun-util.jar;jini-ext.jar;. -Djava.security.policy=policy.all -Djava.rmi.server.codebase=http://localhost:8080/ SavingsCalculatorClient 1000 100 4 10 Listing 10: running the client Conclusion It is very early to see how Jini will evolve. The framework is complete, easy to use and powerful. If Sun enjoys enough support, it can turn Jini into a killer application for distributed computing.
Benoît Marchal runs his own consulting company, Pineapplesoft. His interests include distributed applications, object-oriented programming and design, system programming, handhelds, and computer languages (notably including Java). He also enjoys teaching and writing. [email protected] Copyright © 1999, Pineapplesoft sprl Jini Part 2
Benoît Marchal Jini, Part 2 This article is the second installment in a series on Jini. Jini, is a comprehensive object- oriented framework to build distributed applications. Jini extends the Java paradigm, "write once run anywhere" to "write once run from anywhere." Jini extends the familiar RMI (Remote Method Invocation) protocol with lookup, leasing and transaction services. In our first article, we saw how to get started with Jini. We first downloaded and installed the Jini packages. Then we learned how to write a simple Jini application. In this article, we will take a closer look at the important discovery and join protocols. You won't see much new code in this article but it should help you understand what is happening with last month's application. If you have not done so already, I strongly advise that you read last month's article. Lookup Last month, we saw that Jini provides a number of services to distributed applications. In particular, Jini builds on the concept of a federation of devices and software components. These devices and software components make services available on the network. The services can be hardware facilities (such as printing, scanning) or software services (such as intensive computing services or user-interfaces). When a new device or a new component is plugged into the network, it immediately exposes its services to the devices already connected. This establishes a very dynamic federation of Java virtual machines that provide services to each other. The federation is dynamic in nature because services can be added or removed at any time. It is therefore important that applications discover services at run-time, with little prior knowledge. As we saw last month, to accomplish this, Jini builds on the Java platform and its capability to download software at run-time. To support this very dynamic environment, we need a flexible mechanism for devices and components to expose their services. Three components provide this mechanism: lookup, discovery and join. Discovery and join both rely on lookup, so we will first study lookup. New Jini services must register with the Jini run-time lookup services. Lookup is a regular Jini service. However it does not provide for printing services or intensive computing. Lookup records information about the various servers on the network. It is through lookup that the clients can find other services. When a device or a software component is plugged into the network, it registers its services with the various lookup services already on the network. Service clients locate services by querying lookup services. You should already be familiar with lookup services since we used reggie last month. Reggie is the default implementation of a lookup service that is provided by Sun Microsystems. It is interesting to notice that the lookup service is a regular service. It builds on the same concepts as the Jini services you write: RMI, the Java platform and code downloading. The only way the lookup service differs from the services that you write is that the lookup interface has been defined by Sun so all Jini applications know about it. Lookup services can organize the Jini services in groups. The grouping helps administrators organize the various devices on their network. A service can be part of (join in Jini jargon) several groups. For example, a Jini-enabled television can join the "Entertainment" group but also the "Living Room Devices" group. Jini does not predefine the groups, it is the responsibility of the administrator to define groups that make sense for the environment. However, Jini provides a default group, known as the "public" group, that devices should join by default. The administrator can later move the devices from the default public group to a different one. For example, when you connect a new hi-fi set, it will first appear in the default public group but you can move it to the "Entertainment", "Music" and "Living Room Devices" groups as appropriate. Discovery The lookup concept is very simple however there's a "chicken and egg" problem. How do you find the lookup service? In theory Jini clients find services through the lookup service. It seems logical they should use a lookup service to access the lookup services. But wait, that can't work! How do you get started? How do you find the lookup service itself? The answer lies in the discovery protocol, arguably one of the most important Jini protocols. Through discovery, devices and components can find the lookup services available on the network. Once a device or a component has discovered lookup services, it can register its own services or join services. To join a lookup service, the device or application sends information about itself and the services it supports. Discovery is a boostrap protocol, a protocol that helps get started. There are many different network organizations. Discovery comes in three shapes: two forms of multicast protocols appropriate for local-area networks and unicast for contacting non-local lookup services. Multicast Request Multicast allows devices and components to advertise their presence on a local area network. On startup, devices and components send several multicast UDP packets to a well-known port. By definition, multicast packets are received by every station on the local-area network therefore if there is a lookup service somewhere on the network, it will hear about the device. Lookup services listen on the well-known port and contact the sender, using the unicast protocol (next section), to request more information. See Figure 1 for an illustration of a multicast request.
Figure 1: A Multicast Request The multicast request is analogous to shouting "Is anybody out there?" and waiting until someone answers. Multicast is dynamic since it requires no previous knowledge on behalf of either of the two parties. However it requires that every device or component reside on the same network; which is not always possible. Unicast Request Unicast, on the other hand, requires that devices or component know the address of the lookup services. A device or component sends a TCP request to the lookup service. The lookup service replies with an instance of ServiceRegistrar -- an RMI object. Figure 2 illustrates this case.
Figure 2: A Unicast Request Unicast establishes a permanent connection between the two parties and is therefore more efficient for subsequent transfer of data. However unicast only works if one party knows the address of the other. In practice, either the user must provide the address of the lookup service or unicast is used in conjunction with a multicast protocol. For example, the lookup service uses unicast to reply to multicast requests. Unicast is also helpful if the lookup service is outside the multicast range, e.g. if it is on a different local area network. Multicast Announcement There is one more form of discovery, the multicast announcement. Multicast announcements are used by lookup services when they are first connected (or brought back after a failure) to announce their presence on the network. Devices or applications that are interested in the lookup service reply, with an unicast reply. Figure 3 illustrates multicast announcement.
Image 3: Multicast Announcement In practice, devices and components use both multicast requests and multicast announcements. At startup, devices and components actively seek lookup services using multicast requests. They should discover all the lookup services available on the network within a short period of time. After this time, they stop sending requests and switch to passively listening for multicast announcements. LookupDiscovery
Both multicast requests and announcements are implemented in the LookupDiscovery class. LookupDiscovery first attempts to locate lookup services using multicast requests. After a few moments, it should find all the lookup services on the network. It then switches to passively listening to multicast announcements in case a new lookup service is added to the network.
LookupDiscovery takes an instance of DiscoverListener in its constructor. It sends the discovered and discarded messages when it discovers or looses track of lookup services respectively. Listing 1 illustrates how to use LookupDiscovery to print a list of all the lookup services on the network. Note the use of Thread.currentThread().join() so this application does not terminate. import java.rmi.*; import net.jini.core.lookup.*; import net.jini.core.discovery.*; import net.jini.discovery.*; public class DoMulticast implements DiscoveryListener { public static void main(String[] args) throws Exception { System.setSecurityManager(new RMISecurityManager()); LookupDiscovery ld = new LookupDiscovery(LookupDiscovery.NO_GROUPS); ld.addDiscoveryListener(new DoMulticast()); ld.setGroups(LookupDiscovery.ALL_GROUPS); Thread.currentThread().join(); } public void discovered(DiscoveryEvent de) { try { // Invoke getRegistrar() on the LookupLocator // to perform unicast discovery of the lookup service. ServiceRegistrar[] registrars = de.getRegistrars(); for(int i = 0;i < registrars.length;i++) System.out.println(registrars[i].getLocator()); } catch (Exception e) { e.printStackTrace(); } } public void discarded(DiscoveryEvent de) { } } Listing 1: DoMulticast.java
To compile and run the DoMulticast application, do the following (for more detailed information, refer to the first article in this Jini series):
1. To compile the application: javac -classpath jini-core.jar;sun-util.jar;jini-ext.jar;. DoMulticast.java 2. To start the web server: 3. copy reggie-dl.jar web java -jar tools.jar -port 8080 -dir c:\web -verbose 4. To start the RMI activation daemon. You will have to wait for a few seconds (up to 5 minutes on a slow machine) for rmid to start: 5. echo y | del log 6. rd log rmid 7. To start "reggie": (Unlike the other tools, reggie should not be started in its own console. You should start reggie and wait until the prompt reappears to continue.) (Note: all files are the same as we used last month): 8. echo y | del c:\reggie_log 9. rd c:\reggie_log 10. java -jar -Djava.security.policy=policy.all reggie.jar http://localhost:8080/reggie-dl.jar policy.all c:\reggie_log public 11. Finally, to start DoMulticast: java -classpath jini-core.jar;sun-util.jar;jini-ext.jar;. -Djava.security.policy=policy.all DoMulticast To effectively test DoMulticast, you should start several copies of the lookup service reggie. It is more interesting if you start some instances of reggie before running DoMulticast and others while DoMulticast is running. Warning! Make sure you use different directories for the reggie log file (reggie_log) or you won't be able to start several copies of reggie. For example, try: java -jar -Djava.security.policy=policy.all reggie.jar http://localhost:8080/reggie-dl.jar policy.all c:\reggie_log2 public Join Once a device or a component has discovered a lookup service, it registers its services. At the end of the discovery protocol, the lookup service should have passed an instance of ServiceRegistrar to the device or the component. This interface supports the register() method. register() takes a service ID (null if registering the object for the first time), the service object itself and a set of attributes that describe the object. You have total control over which attributes are associated with each service. By default, Jini recognizes the following attributes: postal address, comment, location (such as "in the living room"), name, generic information such as vendor name, type and status (such as: "error: no CD in player"). Of course, you don't have to attach an instance of every attribute to a service. Furthermore a service can have several instances of the same attribute attached to it. For example, a hi-fi set can have more than one generic information attribute if the speakers are of a different brand than the CD player.
You can also define your own attributes by implementing the Entry interface. Listing 2 shows how to create an owner attribute (an owner attribute indicates the service owner) by extending AbstractEntry. import net.jini.entry.*; public class Owner extends AbstractEntry { private static final long serialVersionUID = 2743215148071307201L; public Owner() { } public Owner(String owner) { this.owner = owner; } public String owner; } Listing 2: Owner.java JoinManager We don't have to implement the discover and the join protocols ourselves. The Jini run- time includes a convenience class, JoinManager that does all the hard work for us. Listing 3 is the main method for the server we wrote last month. It illustrates how to use JoinManager and an array of Entry objects to register with the appropriate lookup services. public static void main(String[] args) throws RemoteException, IOException { System.setSecurityManager(new RMISecurityManager()); SavingsCalculatorImpl sc = new SavingsCalculatorImpl(); Entry[] entries = { new Name("SavingsCalculator"), new ServiceInfo("SavingsCalculator", "Pineapplesoft", "Digital Cat", "1.0", "",""), new Comment("This service demonstrates how to get started with Jini.") }; new JoinManager(sc, entries, sc, new LeaseRenewalManager()); System.out.println("Server ready"); } Listing 3: SavingsCalculatorImpl.java Conclusion In this article, we reviewed the discovery and join protocols in detail. Discovery is a very important protocol because it allows devices and software components to bootstrap themselves in the world of Jini.
Benoît Marchal runs his own consulting company, Pineapplesoft. His interests include distributed applications, object-oriented programming and design, system programming, handhelds, and computer languages (notably including Java). He also enjoys teaching and writing. [email protected] Copyright © 1999, Pineapplesoft sprl http://www.javacats.com/US/articles/Ben/Jini3.html Jini Part 3
Benoît Marchal Jini, Part 3 This is the third article in the Jini series for Digital Cats. In this article, we explore how to take advantage of downloadable code to implement Jini services. As we will see, this provides much more flexibility in the implementation of Jini services. In the process, we look at the join process and reveal the insides of the JoinManager. This article builds on concepts introduced in the first two articles of the series. In particular, it builds on the discussion of the discovery and join protocols in last month. RMI vs Local Objects So far, the Jini services I have discussed have been implemented as RMI client/servers. This is a simple solution but it is not necessarily the most flexible. Indeed there are many cases where we wouldn't want to go through an RMI server. Printer drivers are a good example. Imagine a Jini-enabled printer. When plugged into the network, it announces itself and it registers the services it provides. The services will typically be implemented in a PrinterDriver interface. However a printer driver is not intrinsically a distributed process. A printer driver typically generates a page description in a printer language such as Postscript or HP PCL. It provides methods to write text, select fonts or draw images. The page description is then passed to a print server which collects all the jobs and queues them. This is inherently a local process. Indeed, doing it in a client/server architecture would result in many network calls that would do little more than slow down the process. RMI would be very helpful in writing the print server itself, but it is not useful for printer drivers. In fact, the remote calls would dramatically slow down the preparation of the page description. Clearly a different mechanism is needed. Jini has a concept of local objects. In this mode, the object that implements the interface is not an RMI client/server object. Instead, it's a Java class that is downloaded from the server and executed locally. In a sense, this model is similar to applets which are also downloaded from a server and run locally. A word of warning: the vocabulary is confusing. In Jini parlance, downloading the implementation instead of downloading just the RMI stub, is known as a "local object." This is confusing because the object is not local to the client, it has been downloaded remotely, but it runs locally. So far, we haven't looked in detail into what is happening when a client finds a server. In particular, we haven't really considered why a web server is needed. In fact when the client has found a service and does a lookup, the service returns an object. If the class for the object is not present locally, it is downloaded from the web server. In the first two articles, all the objects where RMI stubs. A stub is an object that forwards all requests to an RMI server. However, in the case of local objects, there is no stub. Instead the real class and an object instance are downloaded. The key point for us is the fact that the process is totally transparent to the client. Indeed the client simply finds a service, downloads an object and uses it. Provided the object conforms to the interface, it works. This is a good example of encapsulation. Writing a Service as a Local Object In this section, I rewrote the compound savings application I have been using since the first article of the series. For more information on what the service does, please read the first article. Interface Like any Jini service, it starts with an interface (see listing 1). The interface is largely unchanged from the first article except that it is no longer an RMI interface. In particular, it no longer implements the Remote interface and the method doesn't throw a RemoteException. public interface SavingsCalculator { public double calculate(double initial, double yearly, double interest, int duration); } Listing 1: SavingsCalculator.java Client The client hasn't changed at all since the first article. Remember, that's the beauty of interface: for the client, it makes no difference whether the service is implemented as an RMI object or as a local one. Whether to use local objects or RMI servers is entirely an implementation issue and the choice is left to the service developer. I have reproduced the client in listing 2 for your convenience. import java.io.*; import java.rmi.*; import net.jini.core.entry.*; import net.jini.core.lookup.*; import net.jini.lookup.entry.*; import net.jini.core.discovery.*; public class SavingsCalculatorClient { protected static void printHelp() { System.out.println("usage is: SavingsCalculatorClient initial monthly interest duration"); } public static void main(String[] args) throws RemoteException, IOException, ClassNotFoundException { System.setSecurityManager(new RMISecurityManager()); try { LookupLocator lookup = new LookupLocator("jini://localhost"); ServiceRegistrar registrar = lookup.getRegistrar(); Entry[] entries = { new Name("SavingsCalculator") }; SavingsCalculator savings = (SavingsCalculator)registrar.lookup(new ServiceTemplate(null,null,entries));
System.out.println(savings.calculate(Double.valueOf(args[0]).doubleValu e(),
Double.valueOf(args[1]).doubleValue(),
Double.valueOf(args[2]).doubleValue(),
Integer.parseInt(args[3]))); } catch(NumberFormatException e) { printHelp(); } catch(ArrayIndexOutOfBoundsException e) { printHelp(); } } } Listing 2: SavingsCalculatorClient.java Server The server has changed more dramatically. Firstly the implementation of the service has been moved out of the server class into a class of its own. This results in a smaller class file which will download faster. The new class is serializable, see listing 3. import java.io.*; public class SavingsCalculatorDriver implements SavingsCalculator, Serializable { static final long serialVersionUID = -3375486113113481755L;
public double calculate(double initial, double yearly, double interest, int duration) { interest = 1.0 + (interest / 100); double ending = initial; for(int k = 0;k < duration;k++) ending = yearly + (interest * ending); return ending; } } Listing 3: SavingsCalculatorDriver.java The server, which contains the logic for join is in listing 4. import java.io.*; import java.rmi.*; import net.jini.core.entry.*; import net.jini.core.lookup.*; import net.jini.lookup.entry.*; import net.jini.discovery.*; import com.sun.jini.lease.*; public class SavingsCalculatorDriverServer implements DiscoveryListener { protected ServiceItem si; protected LeaseRenewalManager lrm = new LeaseRenewalManager();
public SavingsCalculatorDriverServer(ServiceItem si) { this.si = si; }
public void discovered(DiscoveryEvent de) { new Thread(new DiscoveredRunner(de,si,lrm)).start(); }
public void discarded(DiscoveryEvent de) { }
public static void main(String[] args) throws InterruptedException, IOException { System.setSecurityManager(new RMISecurityManager());
SavingsCalculator sc = new SavingsCalculatorDriver(); Entry[] entries = { new Name("SavingsCalculator"), new ServiceInfo("SavingsCalculator", "Pineapplesoft", "Digital Cat", "1.0.1", "",""), new Comment("This service demonstrates how to get started with Jini."), new Comment("This particular implementation downloads the object implementation.") };
// launch the discovery and join LookupDiscovery ld = new LookupDiscovery(new String[]{""}); ServiceItem si = new ServiceItem(null,sc,entries); ld.addDiscoveryListener(new SavingsCalculatorDriverServer(si));
System.out.println("Server ready"); Thread.currentThread().join(); } } class DiscoveredRunner implements Runnable { protected DiscoveryEvent de; protected ServiceItem si; protected LeaseRenewalManager lrm;
public DiscoveredRunner(DiscoveryEvent de, ServiceItem si, LeaseRenewalManager lrm) { this.de = de; this.si = si; this.lrm = lrm; }
public void run() { try { ServiceRegistrar[] registrars = de.getRegistrars(); for(int i = 0;i < registrars.length;i++) { ServiceRegistration sr = registrars[i].register(si,Long.MAX_VALUE); System.out.println("registered on " + registrars[i].getLocator() + " as: " + sr.getServiceID()); lrm.renewUntil(sr.getLease(),Long.MAX_VALUE,null); if(null == si.serviceID) si.serviceID = sr.getServiceID(); } } catch(Exception e) { e.printStackTrace(); } } } Listing 4: SavingsCalculatorDriverServer.java
In the first article, I used the JoinManager class to take care of the join process. JoinManager is a convenience class, it is not part of the core Jini implementation (indeed, it sits in package com.sun.jini.lookup) but it simplifies using the core classes. JoinManager, for some reason, does not seem to work properly with local objects. it was therefore necessary to write my own implementation of the join process. This gives us an opportunity to look under the hood of JoinManager, as my implementation is very similar. This uses concepts that I developed last month.
SavingsCalculatorDriverServer initiates a discovery on the groups to be joined, i.e. the public group. In principle, the service could try to join more groups. The bulk of the code is in the method discovered(). You will remember that discover() is called by LookupDiscovery when it find a lookup service. The main() methods ends with a call to Thread.currentThread().join() which causes the server to wait forever.
Last month, our implementation of discovered() simply iterated over the list of lookup services and printed their characteristics. This month, we do more work as we join each lookup service. However joining services takes time and the documentation for discovered() warns that "The method should return quickly; e.g., it should not make remote calls." To work around this problem, the actual iteration takes place in a separate thread. For more information on multi-threading, consult my Multi-threading in Java article, also on Digital Cats.
The implementation of the thread is in the DiscoveredRunner class which is also in listing 4. For each lookup service, it calls register(). It then pass the newly registered service to LeaseRenewalManager, another convenience class that takes care of lease management. We will study lease management in more detail in coming months.
As you can see, it's not difficult to manage the join process. JoinManager is helpful to start with Jini but it's not very difficult to manage the process ourselves. Note that JoinManager does more things than SavingsCalculatorClient. In particular, it also handles lookup services shutdown. Running the Example To compile and run the application, follow these steps (for more detailed information, refer to the first article in this Jini series):
1. compile the application: javac -classpath jini-core.jar;sun-util.jar;jini-ext.jar;. *.java
2. start the web server: copy reggie-dl.jar c:\web java -jar tools.jar -port 8080 -dir c:\web -verbose
3. start the RMI activation daemon. You will have to wait for a few seconds (up to 5 minutes on a slow machine) for rmid to start: echo y | del log rd log rmid
4. start reggie. Unlike the other tools, reggie should not be started in its own console. You should start reggie and wait until the prompt reappears to continue. The policy.all file is the same file we used last month: echo y | del c:\reggie_log rd c:\reggie_log java -jar -Djava.security.policy=policy.all reggie.jar http://localhost:8080/reggie-dl.jar policy.all c:\reggie_log public
5. start the server: start java -classpath jini-core.jar;jini-ext.jar;sun- util.jar;jini-ext.jar;. -Djava.security.policy=policy.all -Djava.rmi.server.codebase=http://localhost:8080 SavingsCalculatorDriverServer
6. finally start the client: java -classpath jini-core.jar;sun-util.jar;jini-ext.jar;. -Djava.security.policy=policy.all -Djava.rmi.server.codebase=http://localhost:8080/ SavingsCalculatorClient 1000 10 4 5 Where the policy.all file is as listing 5. grant { permission java.security.AllPermission "", ""; }; Listing 5: policy.all Private Protocol There is an interesting extension to the local object model: the idea of using a private protocol to communicate between the client and a server. As you can see, there are no constraints on how the local object should implement the service. In fact it could very well open a raw socket and connect with a server using a proprietary protocol. This would be totally transparent to the client, which still deals with an interface. This is how Jini can connect to legacy applications. The services can be implemented using RMI, but also with CORBA, DCOM or a specific protocol such as XML over HTTP. This also means that a Jini wrapper can be written around any existing distributed application, even if not originally written in Java! To Conclude As we continue to explore Jini, we find how flexible a framework it is for distributed computing. In the first two articles, we explored the power of the join and discovery protocols. In this issue, we learned that Jini supports very flexible service implementations. This flexibility means the programmer can achieve large performance boosts and connect to legacy servers.
Benoît Marchal runs his own consulting company, Pineapplesoft. His interests include distributed applications, object-oriented programming and design, system programming, handhelds, and computer languages (notably including Java). He also enjoys teaching and writing. [email protected] Copyright © 1999, Pineapplesoft sprl Jini Part 4 by Benoît Marchal This is the fourth article in the Jini series. The first three articles focused on the lookup and discovery protocols. This article discusses distributed leasing. Jini is an architecture for distributed applications written in Java. Jini promotes the concept of a federation of services, implemented as a set of virtual machines that collaborate. Jini specifies core services in such an architecture. Reference and Time A distributed system has different constraints than a stand-alone one. In particular, by definition, a Jini system executes on several virtual machines that communicate over the network. Temporary network failures or a machine crash impact the whole system since some services may become unavailable. Therefore distributed systems must have contingencies for error. For example, a stand-alone program is responsible for freeing the objects and resources (memory, files, etc.) it uses. In contrast, in a distributed system, a server routinely allocates objects on behalf of its clients, e.g. it opens database connections or allocates storage. Logically, the objects are used by the clients which should be responsible for freeing them. However network problems, such as congestion, may effectively prevent clients from freeing remote objects. Over time, objects that are no longer in use accumulate on the server, wasting precious resources. Normally the servers try to guess which objects are unused and reclaim them, e.g. it may free objects that remain unused for more than one hour. However the server is only guessing and it may free objects that are still in use somewhere, wreaking havoc on the clients. Conversely, the server may never reclaim some objects even though no clients need them. Distributed leasing solves the distributed resource management issue. Leasing binds the notion of time to a remote object. Instead of obtaining a reference to a remote object, the client leases the object from the server. The lease is only valid for a certain period which is negotiated between the client and the server. Before the lease expires, the client can request an extension. If extensions are granted regularly, the remote object remains available as long as the client needs it. The server knows when it can safely free the object. If the client does not renew the lease, either because of a network problem or because it no longer needs the object, the server knows it can free the object. The client also knows exactly when the server will consider the object unused. Lease Interface The distributed leasing mechanism is surprisingly clean and simple. The complete specification (package net.jini.core.lease) fits in only two interfaces and four exceptions! The most important interface is net.jini.core.lease.Lease which models a lease transaction between the client and server.
Lease has two essential methods: renew() and cancel(). As the name implies, the client calls renew() to negotiate the renewal of the lease. In practice, the client must periodically call renew() before the lease expires. The client can call cancel() to force an early termination of the lease. Calling cancel() when an object is no longer needed is optional because the lease will eventually expire. However, it is good practice to terminate the lease as soon as the object is no longer needed.
The other interface is LeaseMap, which conveniently groups several Lease's and provides methods to renew them collectively (the operation is not atomic however, i.e. some of leases may be renewed while others are not). The distributed leasing is very flexible because it separates the object from its lease. Therefore the client and server can manage leasing independently of the objects. For example, a server can move lease management to a low-priority thread. The server can also temporarily swap objects out of main memory with no impact on leasing. Likewise, the client can use a separate thread to renew the leases. In a more dramatic move, a client can pass its leases to another application before dying. This application could renew the lease until a new client is ready to use the objects! Jini Library Let's illustrate this discussion with a library application. The library server has a number of distributed book objects. Like in a real library, clients can borrow books for a certain period. They can also apply for extensions. The library relies on distributed leasing to manage book borrowing. Obviously no two clients can borrow the same book at the same time. A client may be waiting for another client to return the book. It is therefore very important that books are automatically released when they are no longer in used (real libraries would benefit a lot from this kind of setup). Finally, when a client borrows a book, additional memory is allocated on the server to manage the transaction. Periodically a low-priority thread will clean up the memory for leases that have expired. Sun Helper Classes
Implementing the Lease interface on the server is not very difficult but it is very repetitive coding: nothing looks more like a Lease implementation than another Lease implementation. To save us tedious, repetitive work, Sun supplies a default Lease implementation in the com.sun.jini.lease.landlord package. Note this is a com.sun package to indicate it is vendor-specific goodies and is not part of the Jini specification. To use landlord, I had to implement the Landlord interface. Landlord accepts or refuses lease renewals and that's all it does. All the drudgery of communicating Landlord decisions to the clients is done automatically.
On the client side, Sun proposes the com.sun.jini.lease.LeaseRenewalManager class. LeaseRenewalManager is an old friend that we have been using since the first article in conjunction with the join protocol. LeaseRenewalManager automatically renews the lease on our behalf. In Practice
Now it is time to write the library server. First, I define the Library interface in listing 1. A library is a remote object with one method to borrow books. Note that the borrowBook() method accepts a duration argument for the initial duration of the lease. The method can also throw a LeaseDeniedException if it cannot lease the book. import java.rmi.*; import net.jini.core.lease.*; public interface Library extends Remote { public Book borrowBook(String isbn,long duration) throws RemoteException, LeaseDeniedException; } Listing 1: Library.java Books are also a remote objects that comply with the interface defined in listing 2. In this example, a book has a read-only property for the title and a method to return its Lease object. import java.rmi.*; import net.jini.core.lease.*; public interface Book extends Remote { public String getTitle() throws RemoteException; public Lease getLease() throws RemoteException; } Listing 2: Book.java
The main content of the server is in the LibraryImpl and BookImpl classes. Listing 3 shows LibraryImpl, a remote object that implements two interfaces: Library and Landlord. Library is defined in listing 1. Landlord is described in the Sun Helper Classes section above. import java.rmi.*; import java.util.*; import java.rmi.server.*; import net.jini.core.lease.*; import com.sun.jini.lease.landlord.*; public class LibraryImpl extends UnicastRemoteObject implements Library, Landlord { static final long serialVersionUID = -5980262848862552321L; protected Hashtable books;
public LibraryImpl(Hashtable books) throws RemoteException { super(); this.books = books; Thread cleaner = new Thread(new LibraryCleaner()); cleaner.setDaemon(true); cleaner.start(); }
protected long trimDuration(long duration) { if(Lease.FOREVER == duration || Lease.ANY == duration) return 60000; else return Math.min(60000,duration); }
public Book borrowBook(String isbn,long duration) throws RemoteException, LeaseDeniedException { BookImpl book = (BookImpl)books.get(isbn); if(null != book) { book.lease(isbn,this,trimDuration(duration)); return book; } else throw new LeaseDeniedException("Book unknown"); }
public void cancel(Object cookie) { BookImpl book = (BookImpl)books.get(cookie); if(null != book) book.cancel(); }
public void cancelAll(Object[] cookies) { for(int i = 0;i < cookies.length;i++) cancel(cookies[i]); }
public long renew(Object cookie,long duration) throws LeaseDeniedException { BookImpl book = (BookImpl)books.get(cookie); if(null != book) { long realDuration = trimDuration(duration); book.renew(realDuration); return realDuration; } else throw new LeaseDeniedException("Book has not been leased!"); }
public Landlord.RenewResults renewAll(Object[] cookies,long[] durations) { long[] granted = new long[durations.length]; Exception[] denied = new Exception[durations.length]; for(int i = 0;i protected class LibraryCleaner implements Runnable { public void run() { for(;;) { Enumeration enum = books.elements(); while(enum.hasMoreElements()) ((BookImpl)enum.nextElement()).clean(); try { Thread.sleep(60000); } catch(InterruptedException e) {} } } } } Listing 3: LibraryImpl.java The books are stored in a Hashtable where each book is identified by its ISBN number (ISBN numbers are a standard book identification). borrowBook() searches for the book and tries to lease it. cancel() and cancelAll(), from the Landlord interface, notify the book when its lease has been cancelled while renew() and renewAll() pass on renewal notifications. In this example, lease renewal is trivial: I accept them all but never for more than a minute. The client and the server negotiate the duration of the renewal: the client makes a suggestion but the server only accepts a duration that is smaller or equal to the requested time. If a server accepts a certain duration, it promises to try to keep the object available for that period of time. If the server grants a lease shorter than the client requested, it is likely that the client will ask for renewal. Finally Library uses a cleaner thread. The thread sleeps most of the time. Periodically it goes through all the books and cleans up obsolete leases. To reclaim unused resources, the Lease objects are set to null. Library sets the duration of renewal but delegates the actual management of the Lease objects to BookImpl. I chose this approach because it simplifies multi-threading and synchronization: only one class can manipulate the Lease objects (for more information on multi-threading see my All At Once article. Listing 4 shows BookImpl. import java.io.*; import java.rmi.*; import java.rmi.server.*; import net.jini.core.lease.*; import com.sun.jini.lease.landlord.*; public class BookImpl extends UnicastRemoteObject implements Book, Serializable { static final long serialVersionUID = 6579203593880576795L; protected String title; protected Lease lease = null; protected long expiration = Long.MAX_VALUE; protected static LandlordLeaseFactory factory = new LandlordLease.Factory(); public BookImpl(String title) throws RemoteException { super(); this.title = title; } public String getTitle() { return title; } public synchronized Lease getLease() { return lease; } synchronized void lease(Object cookie, Landlord landlord, long duration) throws LeaseDeniedException { clean(); if(null == lease) { expiration = duration + System.currentTimeMillis(); lease = factory.newLease(cookie,landlord,expiration); } else throw new LeaseDeniedException("Book already leased!"); } synchronized void renew(long duration) { if(null != lease) expiration = duration + System.currentTimeMillis(); } synchronized void cancel() { if(null != lease) { lease = null; expiration = Long.MAX_VALUE; } } synchronized void clean() { if(expiration < System.currentTimeMillis()) { lease = null; expiration = Long.MAX_VALUE; } } } Listing 4: BookImpl.java In addition to storing book titles, BookImpl creates and manages LandlordLease objects. It also tracks when the lease expires (theoretically, the information should be available from the Lease object but LandlordLease is implemented as a local object, see last month's article, and therefore is not updated on the server). The clean() method is called periodically by a background thread to release resources of the Lease that have expired. We are almost finished. Listing 5 shows the server that creates a LibraryImpl. There is nothing new in this class, it takes care of discovery and lookup. import java.rmi.*; import java.util.*; import com.sun.jini.lease.*; import net.jini.core.entry.*; import com.sun.jini.lookup.*; import net.jini.core.lookup.*; import net.jini.lookup.entry.*; public class LibraryServer { public static void main(String[] args) throws Exception { System.setSecurityManager(new RMISecurityManager()); Hashtable books = new Hashtable(); // http://www1.fatbrain.com/FindItNow/Services/home.cl? from=PHP278 books.put("0201310066", new BookImpl("Java Programming Language, Second Edition")); books.put("0471191353", new BookImpl("Constructing Intelligent Agents with Java")); LibraryImpl lib = new LibraryImpl(books); Entry[] entries = { new Name("Electronic library"), new ServiceInfo("Library", "Pineapplesoft", "Digital Cat", "1.0", "",""), }; new JoinManager(lib, entries, new ServiceIDListener() { public void serviceIDNotify(ServiceID id) { System.out.println("service id: " + id); } }, new LeaseRenewalManager()); System.out.println("Server is ready!"); } } Listing 5: LibraryServer.java Finally, listing 6 shows the client. It does a lookup on the server and borrows a book. It also delegates lease renewal to LeaseRenewalManager. In practice, because the library refuses to lease books for more than a minute, the client has to renew the lease frequently. However this is completely hidden by LeaseRenewalManager. import java.rmi.*; import java.net.*; import com.sun.jini.lease.*; import net.jini.core.lease.*; import net.jini.core.entry.*; import net.jini.core.lookup.*; import net.jini.lookup.entry.*; import net.jini.core.discovery.*; public class BookReader { public static void main(String[] args) throws Exception { System.setSecurityManager(new RMISecurityManager()); LookupLocator lookup = new LookupLocator("jini://localhost"); ServiceRegistrar registrar = lookup.getRegistrar(); Entry[] entries = { new Name("Electronic library") }; Library lib = (Library)registrar.lookup(new ServiceTemplate(null,null,entries)); Book book = lib.borrowBook(args.length != 0 ? args[0] : "0201310066",15000); System.out.println("Borrow: " + book.getTitle()); LeaseRenewalManager renewal = new LeaseRenewalManager(book.getLease(),Lease.FOREVER,null); // Thread.sleep(60000); // renewal.cancel(book.getLease()); Thread.currentThread().join(); } } Listing 6: BookReader.java Running the Example Listing 7 is the makefile I use to build the project. CLASSPATH=jini-core.jar;sun-util.jar;jini-ext.jar;classes TARGETPATH=classes SOURCEPATH=src JAVAC=javac JC=$(JAVAC) -d $(TARGETPATH) -classpath $(CLASSPATH) -sourcepath $ (SOURCEPATH) RMIC=rmic RC=$(RMIC) -d $(TARGETPATH) -classpath $(CLASSPATH);rt.jar CLASSES=BookImpl_stub.class LibraryImpl_stub.class \ Library.class Book.class LibraryServer.class BookReader.class .path.java=$(SOURCEPATH) .path.class=$(TARGETPATH) allfiles: $(CLASSES) LibraryImpl_stub.class: LibraryImpl.class $(RC) LibraryImpl BookImpl_stub.class: BookImpl.class $(RC) BookImpl .java.class: $(JC) $< Listing 7: makefile.mak To run the project, issue the following commands: 1. start the web server: 2. copy reggie-dl.jar c:\web java -jar tools.jar -port 8080 -dir c:\web -verbose 3. start the RMI activation daemon. You will have to wait for a few seconds (up to 5 minutes on a slow machine) for rmid to start: 4. echo y | del log 5. rd log rmid 6. start reggie. Unlike the other tools, reggie should not be started in its own console. You should start reggie and wait until the prompt reappears to continue. The policy.all file is the same file we used last month: 7. echo y | del c:\reggie_log 8. rd c:\reggie_log java -Djava.security.policy=policy.all -jar reggie.jar http://localhost:8080/reggie-dl.jar policy.all c:\reggie_log public 9. start the server: start java -classpath jini-core.jar;jini-ext.jar;sun- util.jar;jini-ext.jar;. -Djava.security.policy=policy.all -Djava.rmi.server.codebase=http://localhost:8080 LibraryServer 10. finally start the client: java -classpath jini-core.jar;sun-util.jar;jini-ext.jar;. -Djava.security.policy=policy.all -Djava.rmi.server.codebase=http://localhost:8080/ BookReader 0201310066 To see the distributed leasing in action, try to run a second instance of the client. Because one client has already borrowed the book, the second client will fail with a LeaseDeniedException exception. To experiment with a network failure, simulate one, e.g. kill the first client (Ctrl-C in the console). After about one minute, the lease will expire and the server will reclaim the book. If you launch yet another client, it will successfully borrow the book. You can also uncomment the two lines in BookReader that cancel the lease and recompile. After about one minute, the client will cancel the lease. At that point, run a second client. Guess what happens... I strongly advise you to experiment with leases for yourself. I also suggest you experiment with different values for the lease duration. Conclusion Jini is a comprehensive framework for distributed systems. In this article, we reviewed distributed leasing which adds time constraints to remote object references. Distributed leasing provides a simple but effective mechanism that allows clients and servers to negotiate the lifetime validity of objects. This makes it possible to implement sophisticated resource management that takes into account the possibility of network failure in a distributed system. Jini Part 5 by Benoît Marchal This article, the fifth in the Jini series on Digital Cats, introduces Jini transaction management. Some concepts used in this article were introduced in the first four articles of the series, in particular, the lookup and discovery protocols are covered in part 1, 2 and 3; leasing is covered in part 4. Transaction Management When several entities act concurrently towards a common goal, there is a need to coordinate their activities. Transaction management fulfills that need in distributed systems. Transactions are used to synchronize the work of two or more servers so that they act as one server. Cashing a check is a good example of transaction management. There are two entities involved: the payer's and the payee's banks. Both banks work concurrently towards the common goal: cash the check. Yet, to the check writer, it looks like the two banks work as one: normally the account of the payer is debited while the account of the payee is credited. However if the check bounces, then the payee bank will not credit the check. More specifically, a transaction is a mechanism to group several operations (encoding the check, debiting one account and crediting another) so that they act as a single operation. By "act as a single operation", I mean that the various operations succeed or fail as a whole. Furthermore, if the operations modify storage (e.g. debit an account) then the modification is permanent after the transaction has completed successfully. SQL supports transactions, although they are not distributed. With SQL databases, operations between two "commits" can be undone with a "rollback" command. Therefore the operations between two commits behave as a transaction: they fail or succeed together. Jini transactions work similarly but are distributed. When a client requests that a number of objects work within the context of a single transaction, the objects synchronize with one another through a transaction manager. When the client commits the transaction, the transaction manager uses a two phase commit protocol to implement the transaction semantics. The transaction manager is responsible for coordinating the various objects. However, each object is responsible for managing its own phase, in other words, each object is responsible for the commit or rollback of its own data. More formally, a transaction is defined to have the ACID properties. ACID stands for atomicity, consistency, isolation and durability. Atomicity means that all operations in the transaction occur or none do. Consistency means that, after the transaction, the system is in a consistent state. Consistent in this context can mean technically consistent (e.g. object references are non-null) but also human consistency (e.g. the accounts have been debited and credited with the same amount). Isolation means if there is more than one transaction, they do not interfere with one another. Finally durability means that the results of the transaction are persistent (as persistent as the entity on which the transaction commits). Two Phase Commit As the name implies, the commit is done in two phases or steps. When a client commits a transaction, the transaction manager will query all the objects that are part of the transaction as to whether they are ready to commit or not. An object is ready to commit if it guarantees that it will successfully process a commit request. If all the objects involved in the transaction are ready to commit, the transaction is successful. The transaction manager notifies the object that it can proceed and commit. However, if at least one of the objects is not ready to commit then the transaction as a whole fails. The transaction manager will then ask all the objects to rollback. Figure 1 illustrates a transaction that succeeds. In the first round of interaction, the transaction manager queries the server. In the second round, it commits the transaction. Figure 1: communication between the transaction manager and the servers. How to Use It Jini defines the framework for implementing the transaction semantics in the packages net.jini.core.transaction and net.jini.core.transaction.server. The packages define interfaces and classes for transaction managers and transaction participants. It is up to you to implement the logic behind transaction participants but Sun provides a default implementation for transaction managers known as Maholo. You do not have to use Sun transaction manager but then there is little incentive to write your own. To illustrate transaction management, we will write a simulation of an office. The clerks in the office approve or reject incorporation applications. They will reject an application if the document is not complete. Because applications are complex, more than one clerk checks them, e.g. one clerk checks financial aspects while another makes sure all the proper permits have been delivered. If one or more clerks rejects the application, then the application is rejected. On the other hand, if the application is accepted, then all the clerks dutifully write it in their own record. This office lends itself well to a simulation based on transactions: processing an application is done within the context of a transaction. Jini services simulate clerks; they are handed documents by their boss. As usual, the first step is to define the interface for the clerk service, as shown in listing 1. The interface is simple: a clerk accepts documents within a transaction. The transaction is identified by a transaction manager and a transaction id. import java.rmi.*; import net.jini.core.transaction.*; import net.jini.core.transaction.server.*; public interface Clerk extends Remote { void handle(TransactionManager tm,long tid,Object doc) throws TransactionException, RemoteException; } Listing 1: Clerk.java. Jini Transaction Management The interesting bit, of course, is how the clerk implements the transaction semantics. In the Jini model, the objects are responsible for implementing commit and rollback. Listing 2 shows ClerkImpl. import java.rmi.*; import java.net.*; import java.util.*; import java.rmi.server.*; import com.sun.jini.lease.*; import com.sun.jini.lookup.*; import net.jini.core.lease.*; import net.jini.core.entry.*; import net.jini.core.lookup.*; import net.jini.lookup.entry.*; import net.jini.core.discovery.*; import net.jini.core.transaction.*; import net.jini.core.transaction.server.*; public class ClerkImpl extends UnicastRemoteObject implements Clerk, TransactionParticipant { protected long crashCount = new Random().nextLong(); protected Vector record = new Vector(); protected Dictionary pending = new Hashtable(); public ClerkImpl() throws RemoteException { super(); } public void handle(TransactionManager tm,long tid,Object doc) throws TransactionException, RemoteException { if(null != pending.put(new Long(tid),doc)) throw new TransactionException("Already in progress"); tm.join(tid,this,crashCount); } public void abort(TransactionManager tm,long tid) throws UnknownTransactionException, RemoteException { if(null == pending.remove(new Long(tid))) throw new UnknownTransactionException(String.valueOf(tid)); System.out.println(tid + " aborted"); } public void commit(TransactionManager tm,long tid) throws UnknownTransactionException, RemoteException { Object doc = pending.remove(new Long(tid)); if(null == doc) throw new UnknownTransactionException(String.valueOf(tid)); record.addElement(doc); System.out.println(tid + " committed: " + record.toString()); } public int prepare(TransactionManager tm, long tid) throws UnknownTransactionException, RemoteException { if(Math.random() < 0.8) { System.out.println("I accept " + tid); return PREPARED; } else { System.out.println("I reject " + tid); return ABORTED; } } public int prepareAndCommit(TransactionManager tm, long id) throws UnknownTransactionException, RemoteException { int result = prepare(tm,id); if (PREPARED == result) { commit(tm,id); result = COMMITTED; } return result; } public static void main(String[] args) throws Exception { System.setSecurityManager(new RMISecurityManager()); ClerkImpl clerk = new ClerkImpl(); Entry[] entries = { new Name("Clerk"), new ServiceInfo("Clerk", "Pineapplesoft", "Digital Cats", "1.0", "",""), }; new JoinManager(clerk, entries, new ServiceIDListener() { public void serviceIDNotify(ServiceID id) { System.out.println("service id: " + id); } }, new LeaseRenewalManager()); System.out.println("Server is ready!"); } } Listing 2: ClerkImpl.java. ClerkImpl supports two interfaces: Clerk and TransactionParticipant. Any object that may participate in a transaction must implement TransactionParticipant. The transaction manager will call methods from TransactionParticipant to interact with the object. The transaction is not created by the clerk object but its client (in this case, the boss). An object that participates in a transaction must register itself with the TransactionManager. That is how the transaction manager discovers the parties to the transaction. ClerkImpl does it in the handle() method. Before registering with the transaction manager, however, the clerk object makes a temporary record of the document. Remember that the object must be able to undo the transaction until it succeeds. Therefore, I found it easier to store the document in temporary storage until the transaction is complete. The alternative would be to store the document in final storage and retain enough information to be able to rollback. Whether you need to use temporary storage or use the real storage and retain enough information to undo depends on the specifics of the storage you use. Vectors are not transaction aware, therefore it is not possible to ask a vector to "rollback." On the other hand, databases are transaction aware and they rollback. When using a database as final storage, no temporary storage is required: it is easier to undo. TransactionParticipant defines four methods. The transaction manager first calls prepare(). The object should return TransactionParticipant.PREPARED if it is able to commit the transaction, or TransactionParticipant.ABORTED if not. This is the first phase of the commit. In this simulation, the clerk arbitrarily accepts 80% of all transactions. For the simulation, this is just a random value but it could be more useful processing. The key aspect is that when an object returns TransactionParticipant.PREPARED, it guarantees that it will be able to commit. Depending on how many clerks you use, you may want to experiment with higher and/or lower rates. Note that modern SQL databases support the "prepare to commit" instruction that makes it easy to implement prepare(). Depending on the responses it got from the various objects' prepare(), the transaction manager will call either commit or abort the transaction by calling commit() or abort(), respectively, on all the objects. commit() instructs the object to commit the transaction (the transaction manager knows the object will successfully commit since it returned with a positive result from prepare()). In the simulation, the clerk moves the document from temporary storage to permanent. As the name implies abort() will rollback the transaction. Finally there is the prepareAndCommit() method which groups prepare() and commit() into one operation. The main() method should be familiar by now: it registers the ClerkImpl object as a service. Note that each object will not accept more than one document per transaction. This is not a requirement of Jini transaction management but a simplification I made. In practice, objects typically support more than one operation within a transaction. Where is the Boss? Listing 3 shows the boss object. This is a classical Jini client. It first does a lookup for the transaction manager, then it searches for the available clerks and hands them a document. Finally, it commits the transaction. import java.rmi.*; import java.net.*; import com.sun.jini.lease.*; import net.jini.core.lease.*; import net.jini.core.entry.*; import net.jini.core.lookup.*; import net.jini.lookup.entry.*; import net.jini.core.discovery.*; import net.jini.core.transaction.*; import net.jini.core.transaction.server.*; public class Boss { public static void main(String[] args) throws Exception { System.setSecurityManager(new RMISecurityManager()); LookupLocator lookup = new LookupLocator("jini://localhost"); ServiceRegistrar registrar = lookup.getRegistrar(); Entry[] entries = { new Name("TransactionManager") }; TransactionManager tm = (TransactionManager)registrar.lookup( newServiceTemplate(null,null,entries)); TransactionManager.Created transaction = tm.create(Lease.FOREVER); LeaseRenewalManager renewal = new LeaseRenewalManager(transaction.lease,Lease.FOREVER,null); entries = new Entry[] { new Name("Clerk") }; ServiceMatches sm = registrar.lookup(new ServiceTemplate(null,null,entries),10); String doc = args.length != 0 ? args[0] : "default"; for(int i = 0;i < sm.items.length;i++) { Clerk clerk = (Clerk)sm.items[i].service; clerk.handle(tm,transaction.id,doc); } tm.commit(transaction.id); } } Listing 3: Boss.java. How to Run the Example To run this example, you first need to compile it. Listing 4 shows the makefile I use. CLASSPATH=jini-core.jar;sun-util.jar;jini-ext.jar;classes TARGETPATH=classes SOURCEPATH=src JAVAC=javac JC=$(JAVAC) -d $(TARGETPATH) -classpath $(CLASSPATH) -sourcepath $ (SOURCEPATH) RMIC=rmic RC=$(RMIC) -d $(TARGETPATH) -classpath $(CLASSPATH);c:\jdk1.2\jre\lib\rt.jar JAR=jar CLASSES=Clerk.class ClerkImpl.class Boss.class ClerkImpl_stub.class path.java=$(SOURCEPATH) path.class=$(TARGETPATH) allfiles: $(CLASSES) ClerkImpl_stub.class: ClerkImpl.class $(RC) ClerkImpl .java.class: $(JC) $< Listing 4: makefile.mak. As always, to run the application, you must first copy the stubs to a web server. I use the web server that ships with Jini: start java -jar tools.jar -port 8080 -dir d:\java\jini_office\classes -verbose Next, start the RMI daemon: del log rd log start rmid Then start reggie: del logs\public rd logs\public java -Djava.security.policy=policy.all -jar reggie.jar http://localhost:8080/reggie-dl.jar policy.all d:\java\jini_office\logs\public public You will need a transaction manager to test this example. The Jini development kit ships with Mahalo, which is a basic implementation of a transaction manager. Running Mahalo is easy: del logs\txm\JoinAdminLog rd logs\txm\JoinAdminLog del logs\txm rd logs\txm java -jar -Djava.security.policy=policy.all -Dcom.sun.jini.mahalo.managerName=TransactionManager mahalo.jar http://localhost:8080/mahalo-dl.jar policy.all d:\java\jini_office\logs\txm public Now the infrastructure is in place. You can start several clerk servers with the following command (given you want to test transactions, it is more interesting to run several servers): start java -classpath jini-core.jar;jini-ext.jar;sun-util.jar;classes -Djava.security.policy=policy.all -Djava.rmi.server.codebase=http://localhost:8080 ClerkImpl And finally, run the boss. Make sure you see what happens with the servers: java -classpath jini-core.jar;jini-ext.jar;sun-util.jar;classes -Djava.security.policy=policy.all -Djava.rmi.server.codebase=http://localhost:8080/ Boss "report x" Conclusion Transaction management is very helpful in distributed systems. It is an efficient mechanism when several servers need to cooperate. Jini provides an object-oriented wrapper around the two phase commit protocol. Jini Part 6 by Benoît Marchal We are coming to the end of our study of Jini. The first three articles introduced the lookup and discovery protocols. The fourth article was about leasing, the fifth article discussed transaction management. This article is about distributed events. Events We are familiar with the notion of events as used by JavaBeans or AWT. Events allow an object to notify other objects (known as listeners) when its state changes. Swing and AWT use events to notify the application when the user clicks a button or chooses a menu option. Jini extends the notion of local events to distributed events. Distributed events are similar to local events with the exception that they correspond to changes in the state of an object running in a different virtual machine, potentially on a different computer. This is a source of some of the usual problems with distributed computing, particularly the need to account for the relative unreliability of networks. Event Interfaces The Jini event model is embodied in one interface and two classes: RemoteEventListener is an interface that objects must implement to listen to remote events. The interface defines one method notify(); RemoteEvent is a class whose instances represent the event; EventRegistration is a class whose instances are returned by an event producer when it registers a new listener. The model is based on the familiar JavaBeans events. An object that implements RemoteEventListener registers with the event producer and waits for events. When the producer state changes, the producer fires one or more events by calling RemoteEventListener.notify(). The method expects an instance of RemoteEvent as a parameter. RemoteEvent is an object that describes the event. In particular, it has a ID property that identifies the type of event being dealt with. Although Jini remote events are based on the JavaBeans event model, there are two differences worth noting. First there is no standard mechanism for a listener to register with an event producer. The JavaBeans event model imposes the use of add The interface defines only one method: addRemoteEventListener() which, as the name implies, the listener can call to register with the event producer. Notice it takes a MarshalledObject as a parameter. As we will see, the event producer returns this parameter to the listener with each event. Listing 2 shows the LoginManager object. The main() method should be familiar: it registers the service with the lookup service. import java.io.*; import java.util.*; import java.rmi.*; import java.rmi.server.*; import com.sun.jini.lease.*; import com.sun.jini.lookup.*; import net.jini.core.event.*; import net.jini.core.entry.*; import net.jini.core.lookup.*; import net.jini.lookup.entry.*; public class LoginManager extends UnicastRemoteObject implements RemoteEventProducer { protected BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); protected PrintWriter out = new PrintWriter(System.out); protected Properties passwords; protected long count = 0L; protected Dictionary listeners = new Hashtable(); public LoginManager() throws RemoteException { passwords = new Properties(); // might want a more sophisticated mechanism, e.g. read from encrypted // file or connect to an LDAP server passwords.put("administrator","default"); passwords.put("user","default"); passwords.put("guest","default"); } public void run() throws IOException { while(true) { out.print("username: "); out.flush(); String username = in.readLine(); if(username.equalsIgnoreCase("/quit")) System.exit(1); out.print("password: "); out.flush(); String inPassword = in.readLine(), propertyPassword = passwords.getProperty(username); if(propertyPassword != null && propertyPassword.equals(inPassword)) { out.println("Login successful"); fireRemoteEvent(0); // enable services } else out.println("Login failed."); out.flush(); } } protected void fireRemoteEvent(long id) { Enumeration enum = listeners.keys(); while(enum.hasMoreElements()) { RemoteEventListener listener = (RemoteEventListener)enum.nextElement(); RemoteEvent event = new RemoteEvent(this,id,count, (MarshalledObject)listeners.get(listener)); try { listener.notify(event); } catch(UnknownEventException e) { e.printStackTrace(); } catch(RemoteException e) { e.printStackTrace(); } } count++; } public EventRegistration addRemoteEventListener(RemoteEventListener listener, MarshalledObject handback) throws RemoteException { try { listeners.put(listener,handback); return new EventRegistration(0,this,null,count); } catch(Exception e) { e.printStackTrace(); return null; } } public static void main(String[] args) throws Exception { System.setSecurityManager(new RMISecurityManager()); LoginManager loginManager = new LoginManager(); Entry[] entries = { new Name("LoginManager"), new ServiceInfo("LoginManager", "Pineapplesoft", "Digital Cat", "1.0", "",""), }; new JoinManager(loginManager, entries, null, new LeaseRenewalManager()); loginManager.run(); } } Listing 2: LoginManager.java The run() method is more interesting. It prompts for usernames and passwords. After each login attempt, it fires an event by calling fireRemoteEvent(). The fireRemoteEvent() method accepts a long as a parameter. As discussed previously, remote events are identified by their ID, a long. Note that Jini does not specify the meaning of the ID. Nor does it specify how the event producer and the listener agree on the meaning of these IDs. There are several options: the values can be hard-coded, they can be final static variables attached to a common interface or, in the most complex case, the event producer can export methods that describe the meaning of the IDs. For this example, I have taken the simplest solution and have hard-coded the value: 0 meaning the login attempt failed. I could have defined another event (with another registration method) to fire for a failed login attempt. The event also includes a count. The event producer should use different counts for different events. The count is useful because, due to network latencies, events may arrive twice, they may never arrive or they may arrive out of order. Through the count, the listener can check it has not received the event already. Note that Jini does not specify how the count should be incremented. By default, an event producer guarantees that two different events will have different counts so that the listener can check for duplicates. Here, the event producer offers a stronger guarantee: it guarantees that the event count is always incremented. Therefore a listener can check not only for duplicates but also out- of-order and missing events. The last method of interest is addRemoteEventListener(). The method adds the listener to a vector and returns a EventRegistration. EventRegistration has room for the event ID, the current count value, the event source and a lease. In the example lease is set to null, meaning the registration is valid forever. However, with the now familiar lease object, it is easy to limit registration time. Listing 3 is the event listener. It is very simple: it first looks up the event producer, it registers and waits for event. When an event arrives (indicating successful login), it logs the information to standard output. import java.io.*; import java.util.*; import java.rmi.*; import java.rmi.server.*; import net.jini.core.event.*; import net.jini.core.entry.*; import net.jini.core.lookup.*; import net.jini.lookup.entry.*; import net.jini.core.discovery.*; public class Supervisor extends UnicastRemoteObject implements RemoteEventListener { public Supervisor() throws RemoteException { super(); } public void notify(RemoteEvent event) throws UnknownEventException, RemoteException { try { System.out.print(event.getRegistrationObject().get()); System.out.print(": "); if(0 == event.getID()) System.out.println("Successful login attempt"); } catch(IOException e) { throw new UnknownEventException("IOException: " + e.getMessage()); } catch(ClassNotFoundException e) { throw new UnknownEventException("ClassNotFoundException: " + e.getMessage()); } } public static void main(String[] args) throws Exception { System.setSecurityManager(new RMISecurityManager()); LookupLocator lookup = new LookupLocator("jini://localhost"); ServiceRegistrar registrar = lookup.getRegistrar(); Entry[] entries = new Entry[] { new Name("LoginManager") }; RemoteEventProducer loginManager = (RemoteEventProducer)registrar.lookup(new ServiceTemplate(null,null,entries)); if(loginManager == null) System.out.println("It is null!"); Supervisor s = new Supervisor(); loginManager.addRemoteEventListener(s,new MarshalledObject("server 1")); } } Listing 3: Supervisor.java Supervisor implements the RemoteEventListener interface. It also extends UnicastRemoteObject because it is the server that will called remotely by the event producer. How to Run the Example To run this example, you first need to compile it. Listing 4 is the make file I use. CLASSPATH=c:\jini1_0\lib\jini-core.jar;c:\jini1_0\lib\sun- util.jar;c:\jini1_0\lib\jini-ext.jar;classes TARGETPATH=classes SOURCEPATH=src JAVAC=javac JC=$(JAVAC) -d $(TARGETPATH) -classpath $(CLASSPATH) -sourcepath $ (SOURCEPATH) RMIC=rmic RC=$(RMIC) -d $(TARGETPATH) -classpath $ (CLASSPATH);c:\jdk1.2\jre\lib\rt.jar CLASSES=RemoteEventProducer.class LoginManager_stub.class Supervisor_stub.class .path.java=$(SOURCEPATH) .path.class=$(TARGETPATH) allfiles: $(CLASSES) LoginManager_stub.class: LoginManager.class $(RC) LoginManager Supervisor_stub.class: Supervisor.class $(RC) Supervisor .java.class: $(JC) $< Listing 4: makefile.mak As always, to run the application, you must first make the stubs available through a web server. I use the web server that ships with Jini: start java -jar tools.jar -port 8080 -dir d:\java\jini_office\classes -verbose Next, you must start the RMI daemon: del log rd log start rmid Then reggie: del logs\public rd logs\public java -Djava.security.policy=policy.all -jar reggie.jar http://localhost:8080/reggie-dl.jar policy.all d:\java\jini_office\logs\public public Now the infrastructure is in place. Start the login manager with the following command: start java -classpath jini-core.jar;jini-ext.jar;sun-util.jar;classes -Djava.security.policy=policy.all -Djava.rmi.server.codebase=http://localhost:8080 LoginManager Run the listener: java -classpath jini-core.jar;jini-ext.jar;sun-util.jar;classes -Djava.security.policy=policy.all -Djava.rmi.server.codebase=http://localhost:8080/ Supervisor Move to the server window and try to log in as "administrator" with password of "default". Return to the listener window. It should have received the event and logged it. Conclusion Events are both simple and powerful. Firing events is a very effective mechanism for two applications to communicate. Events are also very efficient because the mechanism limits network traffic to changes in the state of the event producer. Thanks to Jini, it is easy to add distributed events to your applications.