The FreePastry Tutorial.
Version @tutorial_version@; @tutorial_date@. For FreePastry version @freepastry_version@. Maintained by @maintainer@.
Lesson 1
Minimal code to create/join a pastry ring.
Download the tutorial file DistTutorial.java.
In this lesson we are going to use Socket transport layer to communicate. The Socket transport layer is a package within the pastry source tree that is used to communicate between nodes using Internet Protocol (IP). It is located in rice.pastry.socket. Socket uses TCP for all messages except liveness which it uses UDP to accomplish. Before we dive into the code lets start with a short vocabulary lesson:Terms:
- NodeId — The randomly assigned unique identifier of a Node in the FreePastry. Typically a NodeId is 160bit represented by 20 Hex digits. We usually only display the first 6 digits as this is enough to uniquely identify nodes for a pretty large ring.
- NodeIdFactory — This generates your local NodeId. Why do we need a NodeIdFactory? In a real deployment of Pastry, it is critical that one cannot choose their NodeId. To accomplish this you may want to use a Certificate Authority to centrally assign NodeIds. However, for your initial purposes one doesn't need to be able to secure the choice of NodeId. The factory pattern gives us the flexibility we need to change this behavior in the future. Thus we begin with RandomNodeIdFactory which generates a Random node id.
- PastryNode — This is a Node in the network. Your application will send messages through the node and the node will deliver messages to your application.
- NodeHandle — This is a "reference" to a PastryNode. It is how you refer to a specific node in the network. A NodeHandle consists of a NodeId and whatever information the transport layer needs to find the node in the underlieing protocol. In this case an IP address and port. In this lesson, you will use a NodeHandle to bootstrap your node into the ring. You can get the NodeHandle to your local node by calling PastryNode.getLocalHandle(). In the distributed environment, you will need to get a NodeHandle to your bootstrap node(s) by asking the transport layer. In this case you are interested in acquiring a node handle from an IP address and port. The the Socket transport layer uses this information to open a socket to this address:port and requests a copy of the NodeHandle from the remote node. If it doesn't find a node at this location it returns null.
- Bootstrap Node — The node you use to join the ring. When a node starts up, it can either join an existing ring, or start a new one. If you don't have a ring yet, then your choice is narrowed down to starting your own. But once your first node has started a ring, you probably want all new nodes join that one. To join a ring, you need to bootstrap off of any one node in the existing ring. This is the bootstrap node. In the example below, we use the first node that we construct as the bootstrap node. In a real deployment, you may want to cache several nodes so that if you are unable to boot from the first one, you can try others.
- PastryNodeFactory — Constructs and initializes the Pastry Node. This sets up the PastryNode including the transport layer, the Leafset Maintenance protocol and, the RoutingTable Maintenance protocol. These protocols are necessary for a FreePastry deployment to properly maintain the overlay structure. In this example we will use the SocketPastryNodeFactory which as its name implies sets up the pastry node to use the Socket transport layer to communicate with other nodes.
- Daemon thread — Prevents the JVM from exiting after the main method is complete. See java.lang.Thread for more information.
/** * This constructor sets up a PastryNode. It will bootstrap to an * existing ring if it can find one at the specified location, otherwise * it will start a new ring. * * @param bindport the local port to bind to * @param bootaddress the IP:port of the node to boot from * @param env the environment for these nodes */ public DistTutorial(int bindport, InetSocketAddress bootaddress, Environment env) throws Exception { // Generate the NodeIds Randomly NodeIdFactory nidFactory = new RandomNodeIdFactory(env); // construct the PastryNodeFactory, this is how we use rice.pastry.socket PastryNodeFactory factory = new SocketPastryNodeFactory(nidFactory, bindport, env); // This will return null if we there is no node at that location NodeHandle bootHandle = ((SocketPastryNodeFactory)factory).getNodeHandle(bootaddress); // construct a node, passing the null boothandle on the first loop will // cause the node to start its own ring PastryNode node = factory.newNode(bootHandle); // the node may require sending several messages to fully boot into the ring synchronized(node) { while(!node.isReady() && !node.joinFailed()) { // delay so we don't busy-wait node.wait(500); // abort if can't join if (node.joinFailed()) { throw new IOException("Could not join the FreePastry ring. Reason:"+node.joinFailedReason()); } } } System.out.println("Finished creating new node "+node); }
Let's examine each line:
- The arguments that we start with are:
int bindport
— the local port to bind to.InetSocketAddress bootaddress
— The address of our bootstrap node.Environment env
— The environment. See lesson 0.b.
public DistTutorial(int bindport, InetSocketAddress bootaddress, Environment env) throws Exception {
- We begin by constructing our NodeIdFactory, which we are going to need to give to our PastryNodeFactory.
NodeIdFactory nidFactory = new RandomNodeIdFactory(env);
- Give the nidFactory to our SocketPastryNodeFactory. Additionally, we need to tell the SocketPastryNodeFactory what port to bind our pastry node to.
PastryNodeFactory factory = new SocketPastryNodeFactory(nidFactory, bindport, env);
- Turn the bootaddress into a NodeHandle. Note that this call blocks, and can take several seconds to complete. It is opening a socket to the specified address. If there is an error, or no PastryNode found at the bootaddress, then
getNodeHandle()
returns null.
NodeHandle bootHandle = ((SocketPastryNodeFactory)factory).getNodeHandle(bootaddress);
- Finally, create the PastryNode. If bootHandle is null then the factory will start a new ring.
PastryNode node = factory.newNode(bootHandle);
- Even though we don't have an application to run yet, it is important that you are aware of the call to
PastryNode.isReady()
. This method returns false until the node is fully booted into the ring (which entails establishing his neighbor set i.e. the routing table and the leafset). This simple loop is a typical way to wait for the node to fully boot into the ring, and abort with an error message if unsuccessful.
// the node may require sending several messages to fully boot into the ring synchronized(node) { while(!node.isReady() && !node.joinFailed()) { // delay so we don't busy-wait node.wait(500); // abort if can't join if (node.joinFailed()) { throw new IOException("Could not join the FreePastry ring. Reason:"+node.joinFailedReason()); } } }
Why might a join fail? Perhaps the Bootstrap node that you acquired above failed before joining could complete.
main()
method. We need to get 1) the local port to bind to, 2) the IP address of the bootstrap node, and 3) the port of the bootstrap node.
/** * Usage: * java [-cp FreePastry-.jar] rice.tutorial.lesson1.DistTutorial localbindport bootIP bootPort * example java rice.tutorial.DistTutorial 9001 pokey.cs.almamater.edu 9001 */ public static void main(String[] args) throws Exception { // Loads pastry settings Environment env = new Environment(); // disable the UPnP setting (in case you are testing this on a NATted LAN) env.getParameters().setString("nat_search_policy","never"); try { // the port to use locally int bindport = Integer.parseInt(args[0]); // build the bootaddress from the command line args InetAddress bootaddr = InetAddress.getByName(args[1]); int bootport = Integer.parseInt(args[2]); InetSocketAddress bootaddress = new InetSocketAddress(bootaddr,bootport); // launch our node! DistTutorial dt = new DistTutorial(bindport, bootaddress, env); } catch (Exception e) { // remind user how to use System.out.println("Usage:"); System.out.println("java [-cp FreePastry- .jar] rice.tutorial.lesson1.DistTutorial localbindport bootIP bootPort"); System.out.println("example java rice.tutorial.DistTutorial 9001 pokey.cs.almamater.edu 9001"); throw e; } }
Let's examine each line:
- This line constructs the Environment. It has the side effect of starting a daemon thread.
Environment env = new Environment();
- This line disables UPnP firewall checking which will cause you problems if you are running your entire test ring inside a lan.
// disable the UPnP setting (in case you are testing this on a NATted LAN) env.getParameters().setString("nat_search_policy","never");
- This line parses the first command line argument into an int.
int bindport = Integer.parseInt(args[0]);
- These lines parse the IP and port and turn them into an InetSocketAddress.
InetAddress bootaddr = InetAddress.getByName(args[1]); int bootport = Integer.parseInt(args[2]); InetSocketAddress bootaddress = new InetSocketAddress(bootaddr,bootport);
- Finally we execute our constructor.
DistTutorial dt = new DistTutorial(bindport, bootaddress, env);
- We wrap the method with an indication of how to use the program in case the user inputs the wrong args.
try { ... } catch (Exception e) { // remind user how to use System.out.println("Usage:"); System.out.println("java [-cp FreePastry-
.jar] rice.tutorial.lesson1.DistTutorial localbindport bootIP bootPort"); System.out.println("example java rice.tutorial.DistTutorial 9001 pokey.cs.almamater.edu 9001"); throw e; }
Congratulations! You have built code to launch/join a FreePastry ring!
Lesson 2 will aid you in running your code.
Lesson 2
Execute the code to launch your new ring.
This is a short lesson that shows you how to run DistTutorial.java which you created in Lesson 1.Download the tutorial file DistTutorial.java.
After you compile the code, you can either run multiple nodes on 1 computer (but in separate processes) or if you have multiple computers, you can launch them on different machines as long as the computers can communicate with each other via IP. In other words, it won't work if the computers are behind different firewalls.- Step 1: Compile
- Setup your directory as follows:
FreePastry-@freepastry_version@.jar rice/tutorial/lesson1/DistTutorial.java
- Compile the Java sources:
javac -classpath FreePastry-@freepastry_version@.jar rice/tutorial/lesson1/*.java
- Setup your directory as follows:
- Step 2: Launch the bootstrap node.
Even though this is the first node, and therefore we know the bootstrap will
not work, you need to place in a bogus ip/port. A smarter
main()
method in DistTutorial.java could fix this problem.java -cp .:FreePastry-@freepastry_version@.jar rice.tutorial.lesson1.DistTutorial 9001 yourhost.domain 9001
(In the above command, yourhost.domain can be the DNS name of your host or its IP address.) Your output will look something like this:
java -cp .:FreePastry-@freepastry_version@.jar rice.tutorial.lesson1.DistTutorial 9001 10.9.8.7 9001 :1122932166578:Error connecting to address /10.9.8.7:9001: java.net.ConnectException: Connection refused: no further information :1122932166578:No bootstrap node provided, starting a new ring... Finished creating new node SocketNodeHandle (<0xB7E151..>/FOO/10.9.8.7:9001 [-4233509936758121968])
Note that the first 2 lines starting with the current system time are generated by the new logging system. - Step 3: Launch another node.
You can do this on another computer but make sure you fill in the name of the computer from step 2 in bootstraphost.domain.
java -cp .:FreePastry-@freepastry_version@.jar rice.tutorial.lesson1.DistTutorial 9002 bootstraphost.domain 9001
Your output will look something like this:java -cp .:FreePastry-@freepastry_version@.jar rice.tutorial.lesson1.DistTutorial 9002 10.9.8.7 9001 Finished creating new node SocketNodeHandle (<0xE00352..>/FOO/10.9.8.7:9002 [4492232311666603357])
Congratulations! You have just launched your first FreePastry ring!
Lesson 3 will show you how to send and receive messages by creating a commonAPI application.
Lesson 3
Write a simple application using the commonAPI.
Download the tutorial files: DistTutorial.java (changed from Lesson 1!!!), MyApp.java, MyMsg.java into a directory called rice/tutorial/lesson3/.
This tutorial will show you how to create and run your first FreePastry application. You will be able to send/receive messages with this application.Terms:
- CommonAPI—Universal interface to structured overlays. The rice.p2p.commonapi package provides an interface similar to the one provided in 'Towards a Common API for Structured Peer-to-Peer Overlays', by F. Dabek, B. Zhao, P. Druschel, J. Kubiatowicz, and I. Stoica, published in the second International Workshop on Peer-to-Peer Systems, Berkeley, CA, February 2003. The API is designed to allow applications to be written in a protocol-independent manner, allowing the easy migration of applications from Pastry to Chord to CAN to Tapestry, etc.... Applications need only interact with the interfaces in the rice.p2p.commonapi package, and need not worry about anything in the rice.pastry packages.
- Application—A program that runs on a Node. This is an interface which all applications on top of a Node must export. This interface allows the underlying node to deliver message, inform the application of passing messages, and changes in the neighbor nodes. You can have multiple Applications on a single node, and you can even have multiple instances of an Application on the same node. Why would you want multiple instances on the same node? Many applications in FreePastry are intermediate applications that provide a service for higher level applications. It is convenient to have a separate instances of say... Scribe for each higher application that would like to multicast. This way you know that any messages that scribe delivers are in regards to your higher level traffic, and you don't need to distinguish that traffic between multiple high level applications.
- Endpoint.java—This interface represents an endpoint which applications can use to send messages from. An endpoint is obtained by the registerApplication() method in Node. The endpoint represents the applications' view of the world.
- Id.java—This interface is the abstraction of an Id in a structured overlay. The only assumption that is made is that the Id space is circular. An Id could represent a live node in the network, an object in the network, or simply a random Id. NodeId implements this interface.
- Message.java—This interface represents the abstraction of a message in the common API. Thus, messages sent to other nodes should extend or implement this class. FreePastry sends messages around the network by first converting them to a byte stream. By default, FreePastry uses java serialization. The raw serialization tutorial shows you how to use a much more efficient serialization mechanism in FreePastry.
MyMsg
Let's start by taking a look at your message class. In FreePastry a Message is an Object that is Serializable. The transport layer serializes this object into bytes then sends it through the network. When the message is received, it is deserialized back into an Object and delivered to your application.public class MyMsg implements Message {
This class implements rice.p2p.commonapi.Message
. Message
extends Serializable and has a single method: getPriority()
.
Let's take a look at that method now.
For now always return Message.LOW_PRIORITY for your messages. It is important to not set application message priority too high, or you may interfere with Pastry's overlay maintenance traffic that keeps the ring functioning properly.
public int getPriority() { return Message.LOW_PRIORITY; }The "payload" of your message is created by making member variables in your message. Here is the payload for
MyMsg
. An Id is the commonAPI version of a NodeId, it is also used as the "key" when routing.
/** * Where the Message came from. */ Id from; /** * Where the Message is going. */ Id to;We will create a
toString()
so that we can print out the message.
public String toString() { return "MyMsg from "+from+" to "+to; }Finally we have the constructor that loads the payload.
public MyMsg(Id from, Id to) { this.from = from; this.to = to; }
MyApp
Now let's take a look at MyApp. MyApp is designed to log output whenever we send or receive a message. The Endpoint is what we will call on to send messages.protected Endpoint endpoint;The constructor generates an Endpoint from the node. The instance is designed to allow you to run the same app multiple times on the same node. The apps will not receive each other's messages. You will only be able to send messages to apps that generated endpoints with the same instance string. For most of your apps, you will only run one instance, so just make sure the instance is the same on all nodes. Note that there are 2 steps in registration, buildEndpoint() and endpoint.register(). This is because you may need the endpoint to complete construction, but do not want to receive messages until construction is complete. This will become more clear in the Raw Serialization Tutorial.
public MyApp(Node node) { // We are only going to use one instance of this application on each PastryNode this.endpoint = node.buildEndpoint(this, "myinstance"); // the rest of the initialization code could go here // now we can receive messages this.endpoint.register(); }
Sending a message:
In a Distributed Hash Table, or DHT, you typically want to route to the nearest node to the hash of an object that you are interested in. The commonAPI provides you withEndpoint.route()
to accomplish this. This function sends a MyMsg
to an id.
/** * Called to route a message to the id */ public void routeMyMsg(Id id) { System.out.println(this+" sending to "+id); Message msg = new MyMsg(endpoint.getId(), id); endpoint.route(id, msg, null); }Note that
Endpoint.route()
takes 3 arguments. They are:
- Id—the destination of the message (optional)
- Message—the message to send
- NodeHandle—a "hint": the node to route to first (optional)
Endpoint.route()
method to accomplish
this by passing a null
argument as the Id and the target node's NodeHandle as the "hint" argument.
/** * Called to directly send a message to the nh */ public void routeMyMsgDirect(NodeHandle nh) { System.out.println(this+" sending direct to "+nh); Message msg = new MyMsg(endpoint.getId(), nh.getId()); endpoint.route(null, msg, nh); }
Receiving a message:
Simply implement thedeliver()
method as is specified by the Application interface.
/** * Called when we receive a message. */ public void deliver(Id id, Message message) { System.out.println(this+" received "+message); }For now you don't need to worry about the additional methods in the Application interface.
Congratulations, you have an application. Let's integrate it into DistTutorial.java
Here is the new code we will add to the bottom of the DistTutorial constructor:// construct a new MyApp MyApp app = new MyApp(node); // wait 10 seconds env.getTimeSource().sleep(10000); // as long as we're not the first node if (bootHandle != null) { // route 10 messages for (int i = 0; i < 10; i++) { // pick a key at random Id randId = nidFactory.generateNodeId(); // send to that key app.routeMyMsg(randId); // wait a sec env.getTimeSource().sleep(1000); } // wait 10 seconds env.getTimeSource().sleep(10000); // send directly to my leafset LeafSet leafSet = node.getLeafSet(); // this is a typical loop to cover your leafset. Note that if the leafset // overlaps, then duplicate nodes will be sent to twice for (int i=-leafSet.ccwSize(); i<=leafSet.cwSize(); i++) { if (i != 0) { // don't send to self // select the item NodeHandle nh = leafSet.get(i); // send the message directly to the node app.routeMyMsgDirect(nh); // wait a sec env.getTimeSource().sleep(1000); } } }
First we create the MyApp.
// construct a new MyApp MyApp app = new MyApp(node);
Wait 10 seconds. Note that to be compatible with the discreet event simulator, it is important to use FreePastry's virtual clock rather than the system clock. This is why we call env.getTimeSource().sleep() rather than Thread.sleep().
// wait 10 seconds env.getTimeSource().sleep(10000);
Let's send some messages, but only if I am not the first node. After all, if I'm the only node in the ring, it is hardly interesting to send messages to myself.
if (bootHandle != null) {Loop 10 times.
for (int i = 0; i < 10; i++) {Reuse the RandomNodeIdFactory to generate random keys to route to.
Id randId = nidFactory.generateNodeId();Route.
app.routeMyMsg(randId);Wait a second and repeat.
env.getTimeSource().sleep(1000); }
After waiting another 10 seconds, let's send some messages directly to nodes. This section is also going to show you how to access the leafset from the PastryNode. Note that this is a FreePastry specific call, not a commonAPI call.
Get the leafset from the PastryNode:
LeafSet leafSet = node.getLeafSet();Iterate over all of the nodes in the leafset.
for (int i=-leafSet.ccwSize(); i<=leafSet.cwSize(); i++) {Don't send to myself. The local node is node zero in the leafset.
if (i != 0) { // don't send to selfExtract the nodehandle at that index.
NodeHandle nh = leafSet.get(i);Wait a second and repeat.
env.getTimeSource().sleep(1000); }Send the message.
app.routeMyMsgDirect(nh); }Now if you execute this code twice you should get something like:
(for Node1)
java -cp .:FreePastry-@freepastry_version@.jar rice.tutorial.lesson3.DistTutorial 9001 10.9.8.7 9001 :1122933198281:Error connecting to address /10.9.8.7:9001: java.net.ConnectException: Connection refused: no further information :1122933198296:No bootstrap node provided, starting a new ring... Finished creating new node SocketNodeHandle (<0xC20545..>/FOO/10.9.8.7:9001 [-4445364026872145996]) MyApp <0xC20545..> received MyMsg from <0xDD90C6..> to <0xA67C20..> MyApp <0xC20545..> received MyMsg from <0xDD90C6..> to <0xBF799E..> MyApp <0xC20545..> received MyMsg from <0xDD90C6..> to <0xC4BEE7..> MyApp <0xC20545..> received MyMsg from <0xDD90C6..> to <0x86ACA9..> MyApp <0xC20545..> received MyMsg from <0xDD90C6..> to <0x9906E6..> MyApp <0xC20545..> received MyMsg from <0xDD90C6..> to <0x8F5015..> MyApp <0xC20545..> received MyMsg from <0xDD90C6..> to <0xC20545..> MyApp <0xC20545..> received MyMsg from <0xDD90C6..> to <0xC20545..>
(for Node2)
java -cp .:FreePastry-@freepastry_version@.jar rice.tutorial.lesson3.DistTutorial 9002 10.9.8.7 9001 Finished creating new node SocketNodeHandle (<0xDD90C6..>/FOO/10.9.8.7:9002 [5138450490561334965]) MyApp <0xDD90C6..> sending to <0x2E5C63..> MyApp <0xDD90C6..> received MyMsg from <0xDD90C6..> to <0x2E5C63..> MyApp <0xDD90C6..> sending to <0x03045C..> MyApp <0xDD90C6..> received MyMsg from <0xDD90C6..> to <0x03045C..> MyApp <0xDD90C6..> sending to <0xA67C20..> MyApp <0xDD90C6..> sending to <0xF9C506..> MyApp <0xDD90C6..> received MyMsg from <0xDD90C6..> to <0xF9C506..> MyApp <0xDD90C6..> sending to <0xBF799E..> MyApp <0xDD90C6..> sending to <0xC4BEE7..> MyApp <0xDD90C6..> sending to <0x86ACA9..> MyApp <0xDD90C6..> sending to <0x41F900..> MyApp <0xDD90C6..> received MyMsg from <0xDD90C6..> to <0x41F900..> MyApp <0xDD90C6..> sending to <0x9906E6..> MyApp <0xDD90C6..> sending to <0x8F5015..> MyApp <0xDD90C6..> sending direct to [SNH: <0xDD90C6..> -> <0xC20545..>/FOO/10.9.8.7:9001 [-4445364026872145996]] MyApp <0xDD90C6..> sending direct to [SNH: <0xDD90C6..> -> <0xC20545..>/FOO/10.9.8.7:9001 [-4445364026872145996]]