Ubik JNDI

Robust Naming

Ubik JNDI provides robust naming to Ubik RMI servers. By robust, we mean: scalable and reliable. The Ubik JNDI API offers a JNDI server that can be started from the command-line and acts as a registry for remote servers. It is similar to the standard Java RMI registry, yet adheres to the JNDI programming model and sports features that make it suitable for scalable distributed applications:

  • It can be federated with other Ubik JNDI servers in a domain (service bindings are thus replicated, and one given server that can't fullfill a lookup request can ask its siblings for the required service);
  • multiple stubs can be bound under the same name, de facto enabling parallelism;
  • it implements round-robin upon lookup - stubs bound under a given name are rotated at each lookup;
  • bound Ubik RMI stubs are made "naming aware" and perform remote method calls according to two strategies, dubbed "sticky" and "stateless";
  • A-la-Jini discovery of JNDI servers is supported.

Ubik JNDI uses IP multicast by default to implement binding replication and fail-over among multiple Ubik JNDI servers in a domain.

This feature is supported through the EventChannel, which is better documented in the Group Communication section.

As of release 3, Ubik supports discovery and replication over Avis: see the Group Communication section for more details.

Ubik JNDI servers communicate with each other in the following manner:

  • When a client binds a server to the JNDI, the server's stub is sent to the JNDI server. Upon arrival, the latter automatically replicates this binding to its siblings in the domain. Thus, every JNDI server is a copy of all the others, and, we de facto have a replicated JNDI tree.
  • When a Ubik JNDI server appears on the network, it broadcasts its presence on the domain. Thus, all JNDI servers automatically "know" their siblings.
  • It might occur that JNDI servers are desynchronized: a given server might have started hours before another, and thus have stubs that the other does not. In such a case, if a given JNDI server receives a lookup request and does not have a corresponding stub, it queries its siblings. If at least one replicated lookup succeeds, the result is cached locally before being sent to the client.
  • In addition to the above, bound objects are cache locally at the client - they are kept in java.lang.ref.SoftReferences. When a new JNDI server appears in the domain, clients upload their bound objects (stubs) to the new JNDI server.
This behaviour makes developing distributed applications with Ubik a breeze: transparently, clients discover JNDI servers, and JNDI servers discover each other. The application developer is freed from having to deal explicitely with an API to benefit from these essential features; everything happens behind the scenes. See the sections on smart stubs and discovery for more details.

For client applications, looking up services (and binding servers) is done in a manner fully compliant with the JNDI specification:

  • A new InitialContext is created with the appropriated properties;
  • lookup and bind operations are performed using the context.

Usage

Starting the JNDI server

The Ubik distribution comes with a script that allows to start JNDI server instances from the command-line. To start a Ubik JNDI server, go to the bin directory under the directory of your Ubik installation. Type jndi on windows, or sh ./jndi.sh on Unix.

Your application classes need not being present in the JNDI server's classpath.

By default, the server is started on port 1099 and the domain "default". This can be overridden at the command-line (with the -p and -d switches, respectively). Type the -h option (for "help") to see syntax information.

You can additionnally configure the server by providing the path to a Java properties file, using the -f option:

./jndi.sh -f /usr/bin/local/ubik/jndi.properties

The path can be either relative or absolute. The properties corresponding to the above-mentioned command-line switches are the following:

  • ubik.jndi.server.port: corresponds to the port the server should listen on.
  • ubik.jndi.domain: indicates the domain the server is part of.

You can in addition specify any other property that is used to configure the Ubik runtime (see the Consts class for the available properties). This also applies to any property described in the Advanced section of this page.

The properties that are loaded from the configuration will be exported as a set of system properties from within the JNDI server's VM, at startup. Therefore they will be made available to Ubik's internal components that use them.

One of the properties that you will find necessary to configure at times is ubik.rmi.address-pattern

Binding a server to the JNDI tree

A Ubik RMI server can be bound to a Ubik JNDI tree in the following way:

package org.sapia.rmi.hello;

import org.sapia.ubik.rmi.naming.remote.RemoteInitialContextFactory;
import org.sapia.ubik.rmi.server.Hub;

import java.rmi.RemoteException;
import javax.naming.InitialContext;

public class HelloImpl implements Hello{
        
  public String getMessage(){
    return "Hello World";
  }
  
  public static void main(String[] args){
    try{
      Properties props = new Properties();
      
      props.setProperty(InitialContext.PROVIDER_URL, 
             "ubik://localhost:1099/");
      props.setProperty(InitialContext.INITIAL_CONTEXT_FACTORY,
             RemoteInitialContextFactory.class.getName());
      
      InitialContext context = new InitialContext(props);
      
      context.bind("server/hello", new HelloImpl());
     
      while(true){
        Thread.sleep(100000);
      }
    }catch(InterruptedException e){
      System.out.println("terminating");
    }catch(Exception e){
      e.printStackTrace();
    }
  }
}

Conversely, the lookup would be performed as such:

package org.sapia.rmi.hello;

import org.sapia.ubik.rmi.naming.remote.RemoteInitialContextFactory;
import org.sapia.ubik.rmi.server.Hub;

import java.rmi.RemoteException;
import javax.naming.InitialContext;

public class HelloLookup {
        
  public static void main(String[] args){
    try{
      Properties props = new Properties();
      
      props.setProperty(InitialContext.PROVIDER_URL, 
              "ubik://localhost:1099/");
      props.setProperty(InitialContext.INITIAL_CONTEXT_FACTORY,
              RemoteInitialContextFactory.class.getName());
      
      InitialContext context = new InitialContext(props);
      
      Hello hello = (Hello)context.lookup("server/hello");
      
      // do not forget...
      context.close();
      
      System.out.println(hello.getMessage());
    }catch(Exception e){
      e.printStackTrace();
    }
  }
}

Advanced Topics

This section covers more advanced topics regarding Ubik's JNDI.

Multicast Address and Port of the JNDI Server

The multicast address and port of the JNDI server can be specified within the properties that are passed to the JNDI initial context. These properties are the following:

  • ubik.rmi.naming.mcast.address
  • ubik.rmi.naming.mcast.port

If you are using an alernate discovery mechanism, you must configure the JVM properties that specify it - as described in the Group Communication section. See the next section as well for an example.

Using Avis

If you are using the Avis-backed implementation, you have to configure the following as part of the JNDI properties that you specify on the client-side:

  • ubik.rmi.naming.broadcast.provider should be set to ubik.rmi.naming.broadcast.avis
  • ubik.rmi.naming.broadcast.avis.url should be set to the URL of your Avis router.

On the side of the JNDI server, you will have to modify the jndi.sh (or jndi.bat) script to set the above as JVM properties, using the -D switches on the java command line.

For more details about avis, jump to the relevant documentation. In the meantime, here's an example:

public class HelloLookupWithAvis {

	public static void main(String[] args) {
	
    try{
      Properties props = new Properties();
      
      props.setProperty(InitialContext.PROVIDER_URL, 
              "ubik://localhost:1099/");
      props.setProperty(InitialContext.INITIAL_CONTEXT_FACTORY,
              RemoteInitialContextFactory.class.getName());
      
      props.setProperty(Consts.BROADCAST_PROVIDER, 
      				Consts.BROADCAST_PROVIDER_AVIS);
      props.setProperty(Consts.BROADCAST_AVIS_URL, "elvin://localhost");
      
      InitialContext context = new InitialContext(props);
      
      Hello hello = (Hello)context.lookup("server/hello");
      
      // do not forget...
      context.close();
      
      System.out.println(hello.getMessage());
    }catch(Exception e){
      e.printStackTrace();
    }
  }		
}

JNDI Context Builder

The code above is tedious. You can use the JndiContextBuilder class the spare yourself some work. Here's the Avis example of the previous section rewritten with the builder:

public class HelloLookupWithJndiContextBuilder {

	public static void main(String[] args) {
	
    try{
    	
    	Context context = JNDIContextBuilder.newInstance()
          .host("localhost")
          .port(1099)
          .property(Consts.BROADCAST_PROVIDER, Consts.BROADCAST_PROVIDER_AVIS)
          .property(Consts.BROADCAST_AVIS_URL, "elvin://localhost")
          .build();
      
      Hello hello = (Hello)context.lookup("server/hello");
      
      // do not forget...
      context.close();
      
      System.out.println(hello.getMessage());
    }catch(Exception e){
      e.printStackTrace();
    }
  }	
}

The Lookup class

As a complement to the JNDIContextBuilder class, the Lookup class may be used to facilitate JNDI lookups. The class expects a javax.naming.Context (such as one built with the JNDIContextBuilder):

...
import org.sapia.ubik.rmi.naming.remote.Lookup;

Hello hello = Lookup.with(context).name("server/hello").ofClass(Hello.class);

As can be seen, the Lookup class takes the name of the object to lookup, and the type (either class or interface) it should be cast to.

Client-Side Discovery

If the host and port specified in the JNDI provider URL do not match an existing JNDI server, the naming client will try to discover an existing JNDI server in the domain dynamically. This is a very important feature to create robust applications; in production, multiple JNDI servers can work together in a domain and provide a fallback for each other in case of crash; clients will always have a JNDI server that they can lookup.

For client-side discovery to work, one only needs specifying the domain to which the client belongs (which should be the same as the JNDI server(s) to which the client is trying to connect). This is done through a property that is passed through the JNDI initialization properties:

Properties props = new Propertie();

props.setProperty(InitialContext.PROVIDER_URL, 
        "ubik://localhost:1099/");
props.setProperty(InitialContext.INITIAL_CONTEXT_FACTORY,
        RemoteInitialContextFactory.class.getName());
        
// here we set the domain...
props.setProperty("ubik.jndi.domain", "someDomain");
// or: props.setProperty(Consts.UBIK_DOMAIN_NAME, "someDomain");         

InitialContext context = new InitialContext(props);
...

Using the JNDIContextBuilder the above would be:

Context context = JNDIContextBuilder.newInstance()
    .host("localhost").port(1099)
    .domain("someDomain");
    .build();
...

Discovery uses Ubik's default group communication configuration, which relies on UDP and IP multicast. If you wish otherwise, you need to set the appropriate JVM properties. The group communication page has more details about how to proceed.

Embedding a JNDI server

You can embed a JNDI server if you wish, making it potentially more convenient in certain deployment scenarios. To that end, you use the EmbeddedableJNDIServer class, that you instantiate following the steps described below:

1) Create an EventChannel

You need to create the EventChannel instance that the JNDI server will use for group communication. If you will be using the default multicast configuration, you only need using the constructor that takes a domain:

EventChannel channel = new EventChannel("mydomain");
channel.start();

You can also specify an alternate configuration (for example you could rely on Avis for broadcasting, or you could wish that point-to-point communication between members of the cluster be done over UDP). See the Group Communication section for the details.

2) Create the JNDI Server

The next step is to instantiate an EmbeddableJNDIServer, pass it the event channel that you've created, and start the server:

final EmbeddableJNDIServer jndiServer = new EmbeddableJNDIServer(channel, 1099);
jndiServer.start(true);
	
Runtime.getRuntime().addShutdownHook(new Thread() {
		
	@Override
	public void run() {
	  jndiServer.stop();
	}
});

The true flag specifies that the server should be started as a daemon. Note how you're responsible for ensuring the clean shutdown of the server (in this case we're doing it in a JVM shutdown hook). The server's stop() method will also close the EventChannel that was passed to the server's constructor.

Auto-Bind

To ensure that all Ubik RMI servers have a stub at all JNDI server in the domain, the local implementation of Ubik's javax.naming.Context keeps locally (in a java.lang.ref.SoftReference) the stubs that it binds. These stubs are automatically bound by the local implementation to new JNDI servers that appear in the domain.

For this to work, the domain to which the client belongs has to be specified as in the previous section. The difference resides in the fact that the local context must NOT be closed once the binding is done. The context internally maintains an EventChannel that listens for new JNDI servers - the latter broacast an event on startup to indicate their presence. So you must keep your client context and thereafter perform your bindings using that instance. The example below shows this:

Properties props = new Propertie();

props.setProperty(InitialContext.PROVIDER_URL, 
        "ubik://localhost:1099/");
props.setProperty(InitialContext.INITIAL_CONTEXT_FACTORY,
        RemoteInitialContextFactory.class.getName());
        
// here we set the domain...
props.setProperty("ubik.jndi.domain", "someDomain");        

InitialContext context = new InitialContext(props);

context.bind("someServer", new SomeServer());

// here we do not close the context; the context object
// is not GC'd since we loop infinitely in below...
 
while(true){
  Thread.sleep(100000);
}

Smart Stubs

Stubs bound to Ubik JNDI are tweaked according to two strategies: "sticky" and "stateless". In both cases, stubs attempt to recover from server crash by trying to reconnect to an available server; in the latter case, round-robin at the stub is also performed.

A single Ubik RMI stub can be shared by multiple threads.

Sticky Stubs

The "sticky" strategy is the default one. By default, when stubs are bound to Ubik JNDI, they are tweaked in order to contain the URL under which they where bound. Then, when performing remote method calls, they send each call to their server of origin; in case of server crash, they use the URL given to them to perform a re-lookup and acquire a fresh server reference.

This strategy is useful if state must be maintain at the server-side - for example, if implementing a session server.

Stateless Stubs

To benefit from the "stateless" strategy, server implementations must implement the Stateless marker interface.

When stateless servers are bound to the JNDI, their stub is tweaked in order to dynamically discover other servers that appear (under the same name) in the domain. On the client-side, such stubs delegate method calls to their set of known servers in a round-robin fashion.

If no state is maintained at the server, use this strategy. It allows client applications to automatically benefit from the processing power of new servers that appear in the domain.