Make room for JavaSpaces, Part 3

Coordinate your Jini applications with JavaSpaces

The first article in this JavaSpaces thread presented an overview of the JavaSpaces programming model and its simple API. You'll recall that the model is based on spaces -- shared, network-accessible object storage and exchange areas -- through which processes communicate and synchronize their activities. Indeed, coordination is a crucial component of distributed programs, just as it is in any distributed activity.

TEXTBOX: TEXTBOX_HEAD: Make room for JavaSpaces: Read the whole series!

:END_TEXTBOX

Consider cars speeding through an intersection, workers assembling a car, or athletes playing a basketball game: each of those real-world distributed systems requires coordination to function smoothly. So with distributed applications, the processes need to synchronize with one another to succeed at a common task. For example, distributed applications often need to mediate access to a limited set of shared resources, guarantee fair access to those resources, or prevent processes from terminating as they wait for resources that may never become available.

This month I'll show you how to use JavaSpaces to coordinate processes in a networked environment. At the same time, I'll start tying JavaSpaces and Jini closer together. If you've experimented with JavaSpaces, you already know that it is implemented on top of Jini. In fact, a new article, "The Nuts and Bolts of Compiling and Running JavaSpaces Programs" (see Resources), can help you get your JavaSpaces programs up and running, which can be a frustrating task, given all the Jini machinery needed underneath. What may not be as clear is that JavaSpaces is a powerful tool for communication and coordination between a Jini federation's entities. In particular, space-based communication and coordination works well in the Jini networked environment, where entities may rapidly appear and disappear at will (or against their will, in the case of machine, software, or network failures).

In this article, I'll investigate how you can achieve some simple forms of distributed synchronization by using a space. To illustrate, I'll build a distributed multiplayer game, provided as a Jini service, that permits access by a limited number of players at a time. You'll see how to mediate players' access to the game through a space. Before diving into the game service, let's start by taking a quick look at an important concept that you'll use to build synchronization into your distributed programs: how the JavaSpaces API provides basic synchronization on entries for free.

Space operations and synchronization

Coordinating distributed processes can be hard work. In an application that runs on a single machine, the operating system, acting as a centralized manager, can synchronize multiple threads. But in a networked environment, a single point of control doesn't necessarily exist. Processes run on different machines and at their own pace. Unless you want to build a centralized controller to manage those processes, which would introduce an unwelcome bottleneck to the environment, you must build a distributed means of managing their interactions, which can be significantly trickier.

JavaSpaces can ease the synchronization of distributed processes because synchronization is built into the space operations themselves. Recall the basics of the JavaSpaces API: write deposits an object, called an entry, into a space; read makes a copy of an entry in a space (but leaves the object there); and take removes an entry from a space. Let's see how this simple API incorporates the basics of space-based synchronization. Suppose you define a simple Message entry like this:

public class Message implements Entry { public String content;

public Message() { } }

Then you instantiate a Message entry and set its content:

Message msg = new Message(); msg.content = "Hello!";

Assuming you have access to a space object, you call its write method to place a copy of the entry into the space:

space.write(msg, null, Lease.FOREVER);

Now any process with access to the space can create a template and read the entry:

Message template = new Message();
Message result = (Message)space.read(template, null, Long.MAX_VALUE);

Any number of processes can read the entry in the space at a given time. But suppose a process wants to update the entry: the process must first remove the entry from the space, make the changes, and then write the modified entry back to the space:

Message result = (Message)space.take(template, null, Long.MAX_VALUE); result.content = "Goodbye!"; space.write(result, null, Lease.FOREVER);

It's important to note that a process needs to obtain exclusive access to an entry that resides in the space before modifying it. If multiple processes are trying to update the same entry using this code, only one obtains the entry at a time. The others wait during the take operation until the entry has been written back to the space, and then one of the waiting processes takes the entry from the space while the others continue to wait.

The basics of space-based programming synchronization can be quickly summarized: Multiple processes can read an entry in a space at any time. But when a process wants to update an entry, it first has to remove it from the space and thereby gain sole access to it. In other words, the read, take, and write operations enforce coordinated access to entries. Entries and their operations give you everything you need to build more complex coordination schemes. With the basics under your belt, let's start building the game service.

A Jini game service

Consider a distributed multiplayer game, offered as a Jini service, that allows only a set number of players at a time. If the game is already at full capacity, a person wanting to play has to wait until an active player leaves. In its simplest form, given a pool of waiting players, the service admits an arbitrary player to the game; this is the approach I take in the example. But of course, in this scheme some players may wait forever while relative newcomers are allowed to play. If you want to be fair, you could make the waiting players queue up and have your service mediate admission to the game on a first-come, first-served basis. For now let's implement the simpler arbitrary-player approach, using a small amount of JavaSpaces code.

Here is the overview of my game service, depicted in Figure 1: The Jini game service supplies two basic methods, joinGame and leaveGame. When a player wishes to play, he or she looks up a game service provider in the standard Jini way, using a lookup server, and is delivered a proxy object to a game service. (I'm not going to cover the basics of Jini services, lookup, and discovery in this article, so if those concepts and terminology are unfamiliar to you, you may want to make a detour to Resources.) With the proxy in hand, the player calls joinGame to join a specific game. When joinGame succeeds in getting admission to the game (via JavaSpaces, as you'll see shortly), it returns an interface to a remote game object, which can be used to play the game. At this point, the player calls the play method of the game object (the game's only method in my simple example) to begin playing. When the play method returns, the player is finished playing and calls the proxy's leaveGame method. In my example, the player sleeps for a bit and then loops, calling joinGame to continue playing the game.

Figure 1. An overview of the game service example

From the command line, you can start a game service, specifying a name for the game and a maximum number of players. Then you can start as many players for that game as you want: if there are fewer players than the maximum, everyone gets to play at once, but if there are more players than the maximum, your game service will need to coordinate access to the game. You may want to download the full example code (Resources) to play with and examine. Let's take a closer look at how the pieces of the system are implemented and how to use JavaSpaces to coordinate them.

The player

Let's start by looking at the implementation of a player. Here is the basic skeleton of the player code, excluding some of the Jini lookup and discovery details (you can find the full code in

Player.java

from the

code.zip

file in

Resources

):

public class Player implements Runnable { protected String gameName; protected String myName; ... public Player(String gameName, String myName) throws IOException { this.gameName = gameName; this.myName = myName;

... // create a template for locating a GameServiceInterface, set a // security manager, & set up a listener for discovery events // in the Jini public group ... } // This method is called whenever a new lookup service is found protected void lookForService(ServiceRegistrar lookupService) { ... // use a template to search for proxies that implement GameServiceInterface ... GameServiceInterface gameInterface = (GameServiceInterface)proxy; while (true) { Game game = gameInterface.joinGame(gameName); if (game != null) { try { System.out.println("Playing game " + gameName); game.play(myName); } catch (RemoteException e) { e.printStackTrace(); } gameInterface.leaveGame(); } else { System.out.println("Couldn't obtain game " + gameName); } // sleep for 10 seconds before trying to join the game again try { Thread.sleep(10000); } catch (Exception e) { e.printStackTrace(); } } } // Create a Player and start its thread public static void main(String args[]) { if (args.length < 2) { System.out.println("Usage: Player gameName playerName"); System.exit(1); } try { Player player = new Player(args[0], args[1]); new Thread(player).start(); // start a thread that sleeps } catch (IOException e) { System.out.println("Couldn't create player: " + e.getMessage()); } } }

As you can see in the player's main method, you instantiate a new Player object, passing it the command-line arguments that hold the name of the game that the player wishes to join and the player's name, which the constructor then assigns to member variables. The player constructor also takes care of Jini details, such as creating a template for locating a GameServiceInterface, setting a security manager, and registering a listener to listen for Jini discovery events.

After the constructor returns, you start a thread that will sleep indefinitely with the help of a loop. This might seem a bit peculiar, but if you don't start the thread, the constructor will finish and the player will simply exit. The thread keeps the player alive and listening for Jini discovery events, which is what you want.

Whenever a lookup service is found, the registered listener (an inner class of player that I am not showing here) is contacted with the event and, in turn, calls the lookForService method that you see above. Since the player wants to find a game service that provides immediate access to a game, this method uses a template to search for proxies registered with the lookup service that implement the GameServiceInterface.

Once a proxy has been located, the player enters a loop. First, the player calls the proxy's joinGame method -- waiting as long as necessary to obtain admission to the game with the given name. Once the player has been admitted, the method returns a remote object that gets assigned to the game. Assuming that the game isn't null, the player calls the remote object's play method to start playing the game. When finished playing, the player calls the game proxy's leaveGame method, which essentially relinquishes the player's slot in the game, making it available to other potential players. The player then sleeps for 10 seconds (apparently, playing games is tiring!) before beginning the loop anew and trying to join the game again.

The game service

All the Jini responsibilities of a game service, including finding Jini lookup services and publishing the proxy used to connect to a game, are handled by the GameService class. Without some of the nitty-gritty Jini lookup and discovery code (which you can peruse in GameService.java), here's what the game service code looks like:

1 2 3 Page
Recommended
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more