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.
|