/home/projects/ubik

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 UDP multicast to implement binding replication and fail-over among multiple Ubik JNDI servers in a domain.

Needless to say, to benefit from Ubik's most interesting features, your network needs to be multicast-enabled. Usually, routers support multicast natively - even domestic routers that you use at home to serve as a gateway between your PC and the internet.

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. Type the -h option (for "help") to see syntax information.

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 Propertie();
      
      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(RemoteException 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 Propertie();
      
      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(RemoteException e){
      e.printStackTrace();
    }
  }
}

Advanced Issues

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

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, through multicast. 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, 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");        

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

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 a multicast server 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.

Attributes

It is now possible to bind and lookup items to/from the JNDI using additional attributes, specified in the JNDI name. This allows distinguishing services bound under the same name, but that may be additionaly qualified in order to be retrieved with a criteria-based approach. The following illustrates how to specify attributes when binding a service to the JNDI:

// the ctx variable is an InitialContext instance, 
/ acquired as shown further above.

ctx.rebind("services/myService?version=1.0&vmId=10933909", 
           myService);

Attributes are specified in a form of a query string; of course, in this case, the string is not intended to make a query, but to further qualify the service that we are binding. Eventually, a similar string can be used to perform a lookup for a service instance that matches specified attributes; in such a case, then, the string acts as a query:

MyService svc = (MyService)ctx.lookup("services/myService?version=1.0");

In the above code, we attempt to retrieve an object bound under the "services/myService" name, but that possesses an additional attribute (version) of a given value (1.0). The query string, in this case, indeed consists of a query. The naming service will return an object that matches the specified name and attributes - or throw a NameNotFoundException. Of course, if many objects match the given query, the returned one is chosen according to a round-robin algortithm.

This mechanism can be very powerful, and is analoguous to Jini templates. When could you use it? Well, the versioning example above is a good candidate: different client applications could require different versions of the same service. Another good use is when you have different development teams requiring the same types of services, but in isolation from one another (a given service instance should be used by a given team, and only that team); this isolation might be necessary so that teams do not "pollute" each other's environment. In this case, we could bind services in a manner similar to the following:

ctx.rebind("services/myService?version=1.0&vmId=10933909&team=mercury", 
           myService);

The example above suggests that teams are given some name that is used as an attribute in the services that they use. Each team in this case would be mandated to specify that attribute when looking up services.