Cookbook

This page provides a few recipes that should help developers get started with Ubik. It does not attempt to be RMI for dummies. For a RMI primer, see Sun's Java Tutorial. The Ubik RMI overview also provides essential code snippets. Rather than teaching the basics, this cookbook will explain how to work with the features of Ubik RMI that are not present in the JDK's RMI.

A Note on Configuration

In general, Ubik is configured with system properties, and allows overriding, for certain components of the system, at the instance level. The framework is meant to provide remoting support for a whole JVM, and it therefore made sense to support configuration through system properties.

Most properties are defined as part of the org.sapia.ubik.rmi.Consts interface, except for specific parts of the framework that represent implementation-specific sub-components (in such case, the properties are defined in interfaces for those sub-components).

You will see certain constructors taking a org.sapia.ubik.util.Conf instance as an argument: this allows passing properties which override system properties. In most cases though it is recommended to configure Ubik through properties defined for the whole JVM (that is, through system properties).

Basic Stuff

Let's start with some basics. You've seem some code lying around throughout the Ubik docs. Here we're trying to be more focused, starting with the basics.

Bind Address Selection

Ubik will attempt exporting remote objects using a server bound to the first network interface that it detects which does not correspond to localhost. If the host does not have such a network interface available, then Ubik resorts to localhost. If multiple network interfaces (other than localhost) are available on the host on which a Ubik server is started, then a regular expression can be used to indicate to Ubik which one to use. That regular expression must be specified as a JVM property, under the ubik.rmi.address-pattern key. If no address could be found that matches the given regexp, then Ubik also resorts to localhost. Here are a few regexps:

\\d{3}\\.\\d{3}\\.\\d{3}\\.\\d{3}
10\\.10\\.\\d+\\.\\d+
192\\.168\\.\\d+\\.\\d+

Note the double backslashes: in Java the backslash is an escape character, therefore it must be doubled in order to be treated as a normal character.

Also, note that you can specify multiple such patterns, by using suffixes, as follows:

ubik.rmi.address-pattern.01=10\\.10\\.\\d+\\.\\d+
ubik.rmi.address-pattern.02=192\\.168\\.\\d+\\.\\d+

In the above case, attempt will be made to find the appropriate interface (network interfaces will be searched according to the lexicographical order of the pattern key suffix).

The Hub

The org.sapia.ubik.rmi.server.Hub keeps Ubik's state for a JVM. You use ubik to export a remote object, as so:

WorkService myRemoteObject = WorkServiceImpl();
WorkService stub = Hub.export(myRemoteObject);

As you can see above, the Hub's export method returns the stub that was created. You could then bind that stub to Ubik's distributed JNDI tree, as explained further below.

As soon as you export an object through the Hub, its internal components are started. You may also invoke the start method explicitely, before anything else, just to manifest an intention:

Hub.start();

By the same token, upon exiting, your application should call the Hub's shutdown method:

Hub.shutdown();

The above must be done whether you've invoked the start method of the Hub class or not.

Implementing Remote Objects

There are two types of remote objects: those that are bound to Ubik's JNDI, and those that are returned by remote method calls.

Remote Interface

Contrary to the JDK's RMI, remote object classes do not have to implement the java.rmi.Remote interface if their instances are bound to Ubik's JNDI. That is, if a remote object corresponds to the return value of a remote method call, its class must implement the java.rmi.Remote marker interface:

public interface WorkService {

  public WorkResult performWork(Work w);

}          

...

public class WorkResultImpl implements WorkResult, Remote {
  ...
}

Remote Annotation

Rather than implementing the Remote interface, you could also annotate your class with the org.sapia.ubik.rmi.Remote annotation:

@Remote(WorkResult.class)
public class WorkResultImpl implements WorkResult {
  ...
}

The annotation requires that you specify which interfaces implemented by your class should also be implemented by the stubs generated for it.

Stateful vs Stateless

Ubik's JNDI supports multiple objects under the same name. Stubs bound to the JNDI tree are enriched in a special way, as to make them either stateful or stateless:

  • Stateless stubs implement automatic failover and load-balancing over servers that expose remote objects under the same name. This implies a stateless behavior: it is expected then that each remote method call can be executed at any of the servers, independently, without side effects.
  • Statefull stubs implement automatic failover, but no load-balancing. This is the default behavior: in such a case, stubs perform remote method calls against the same server, until that server disappears. When that occurs, stubs fail over to the "next" server.

For scalability purposes, it is much preferrable to go with stateless stubs. In order for remote objects to be recognized as stateless when they are bound to Ubik's JNDI, their class or one of their interfaces must implement the Stateless marker interface, as shown below:

public interface WorkService extends Stateless {

  public WorkResult performWork(Work w);

}

Exporting Remote Objects

You've seen it in the previous section: you export an object through the Hub (or when you bind it to Ubik's distributed JNDI tree). When you do so, a stub (that is, a dynamic proxy which represents it and is meant to relay remote method calls to it over the network) is created for it which implements all the object's interfaces.

Ubik supports multiple transport types. Yet when you export an object through the Hub, if an object has already been exported for the transport you've chosen, the server that's been started for this transport will be used - even if, for example, you specify a different port in your second export call. See the next section about the Apache Mina transport, there are more details addressing this characteristic.

The Default Transport: Apache Mina

By default, Ubik uses Apache Mina. That is, when you use the single-argument export method (which you've seen used in the previous section), or the one that takes a port as an additional argument:

WorkService myRemoteObject = WorkServiceImpl();
WorkService stub = Hub.export(myRemoteObject, 4343);

If you rather call this method, a random port is internally picked:

WorkService myRemoteObject = WorkServiceImpl();
WorkService stub = Hub.export(myRemoteObject);

Say the previous code is followed by this one:

WorkService mySecondRemoteObject = WorkServiceImpl();
WorkService mySecondStub = Hub.export(myRemoteObject, 4343);

In the above case, no new server will be started, and therefore no server will listen on port 4343.

That is because Ubik will start only one server per transport type, and serve all remote invocations for objects exported though that transport with the same server.

Netty 4

The following exports an object using the Netty transport, on a random port:

Properties props = new Properties();
props.setProperty("ubik.rmi.transport.type", "tcp/nio/netty");
WorkService myRemoteObject = WorkServiceImpl();
WorkService stub = Hub.export(myRemoteObject, props);

The ubik.rmi.transport.type property indicates which tranport type to use. More conveniently, you can use constants in your code, as follows:

Properties props = new Properties();
props.setProperty(Consts.TRANSPORT_TYPE, NettyTransportProvider.TRANSPORT_TYPE);
WorkService myRemoteObject = WorkServiceImpl();
WorkService stub = Hub.export(myRemoteObject, props);

You can also specify the port to which your-Netty based server should be bound:

Properties props = new Properties();
props.setProperty(Consts.TRANSPORT_TYPE, NettyTransportProvider.TRANSPORT_TYPE);
props.setProperty(NettyConsts.SERVER_PORT_KEY, "4343");
WorkService myRemoteObject = WorkServiceImpl();
WorkService stub = Hub.export(myRemoteObject, props);

HTTP using Simple

Ubik supports remote method invocation over HTTP, through the very convenient Simple framework, which sports a high-performance embeddable HTTP server. In a manner similar to using Netty, you can export a remote object using the HTTP transport as follows - in this case a random port will be used:

Properties props = new Properties();
props.setProperty(Consts.TRANSPORT_TYPE, HttpTransportProvider.TRANSPORT_TYPE);
WorkService myRemoteObject = WorkServiceImpl();
WorkService stub = Hub.export(myRemoteObject, props);

To specify a port explicitely:

Properties props = new Properties();
props.setProperty(Consts.TRANSPORT_TYPE, HttpTransportProvider.TRANSPORT_TYPE);
props.setProperty(HttpConsts.HTTP_PORT_KEY, "8080");
WorkService myRemoteObject = WorkServiceImpl();
WorkService stub = Hub.export(myRemoteObject, props);

Group Communication

Ubik's robustness characteristics rely on group communication. Such group communication is based on the org.sapia.ubik.mcast.EventChannel class. As explained in the relevant documentation, the class supports unicast and broadcast communication. An EventChannel is used behind the scenes to coordinate communications between distributed JNDI nodes and smart stubs.

Therefore, the first thing that you must do when using Ubik is to set up the configuration of the event channel. The examples below show you how.

Default Setup: IP Multicast and TCP unicast

When creating an EventChannel with the constructor that takes only the domain name as argument, the channel will IP multicast for broadcast, and the TCP org.sapia.ubik.mcast.UnicastDispatcher based on Apache Mina. The code below creates such an EventChannel:

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

Customized EventChannel Setup

Of course, the setup can be customized. To use the org.sapia.ubik.mcast.avis.AvisBroadcastDispatcher (which depends on the Avis router and allows deploying broadcast over TCP), define the following system property:

ubik.rmi.naming.broadcast.provider=ubik.rmi.naming.broadcast.avis

Then, instantiate the EventChannel as previously done:

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

Terminating an EventChannel

If your code has created an EventChannel, then it should also terminate it, as follows:

channel.close();

Synchronous Remote Events

The send methods allow sending org.sapia.ubik.mcast.RemoteEvents to one or more nodes, and expect a response from each of these nodes in return. Events received in such a manner at the target nodes are thus dispatched to the org.sapia.ubik.mcast.SyncEventListener that is listening to such events, at that node. The response is synchronously sent back to the EventChannel from which the RemoteEvent originated. That EventChannel is thus responsible for performing aggregation of the different responses, returning these to application code (i.e.: the code that initially invoked the send method).

The following snippets illustrate how to implement a SyncEventListener, and how to send RemoteEvents to it:

import org.sapia.ubik.mcast.*;

// firt implement the listener

public class MySyncEventListener implements SyncEventListener {
  public Object onSyncEvent(RemoteEvent evt) {
    return "WORLD";
  }
}

// register the listener with its EventChannel
// you must keep a reference on the listener, for the channel keeps
// those in weak references

listener = new MySyncEventListener();
recevingChannel.registerSyncListener("ubik.example.sync.event", listener);  

// now, on the other end, send the remote event and handle the response:

Response res = sendingChannel.send(remoteAddress, "ubik.example.sync.event", "HELLO");
if (res.isError()) {
  // handle error
} else if (res.getStatus() == Status.SUSPECT) {
  // remote node did not reply: probably down
} else {
  System.out.println(res.getData());
}

Response timeout is managed globally, through the ubik.rmi.naming.mcast.response.timeout property.

Asynchronous Remote Events

The dispatch methods in turn allow sending RemoteEvents to all, one, or many nodes, asynchronously (no response is expected on this case).

Here's how org.sapia.ubik.mcast.AsyncEventListeners are dealt with:

import org.sapia.ubik.mcast.*;

// firt implement the listener

public class MyAsyncEventListener implements AsyncEventListener {
  public void onAsyncEvent(RemoteEvent evt) {
    try {
      System.out.println("Received a remote event " + evt.getType() + ": " + evt.getData());      
    } catch (Exception e) {
      // deserialization of remote event data may throw IOExceptions and ClassNotFoundExceptions
      e.printStackTrace();
    }
  }
}
 
// register the listener with its EventChannel
listener = new MyAsyncEventListener();
recevingChannel.registerAsyncListener("ubik.example.async.event", listener);  
 
// on the other end, send the event
sendingChannel.dispatch("ubik.example.async.event", "HELLO");

RemoteEvents received in the onAsyncEvent method should be handled in a separated thread, if processing risks blocking for too long.

Distributed JNDI

To expose the smart stubs created by Ubik as remote services, you must use Ubik's distributed JNDI. The org.sapia.ubik.rmi.naming.remote.EmbeddableJNDIServer, as its name implies, can be embedded in your application. An EmbeddableJNDIServer uses an EventChannel to discover other JNDI server nodes on the network. Alltogether, they form a distributed JNDI tree across which smart stubs are replicated.

There's a section of the documentation that provides more details regarding Ubik's JNDI implementation, including additional code samples.

Creating and Starting a JNDI Server

You embed a JNDI server instance in code as the following snippet illustrates. This starts a server on the port you choose, on the domain specified. The server relies on an EventChannel to discover other instances in the domain.

EventChannel channel = new EventChannel("myDomain"); 
channel.start();
EmbeddableJNDIServer jndi = new EmbeddableJndiServer(channel.getReference(), 1099);
jndi.start(true); // true: server will start as daemon

// when done:
jndi.stop();
channel.close();

Both the lifecyle of the EventChannel and of the JNDI server follow the one of your application. Don't forget to dispose of those resources when your application terminates.

Multiple JNDI servers started across different hosts, using the same EventChannel configuration (domain, etc.), will de facto form a distributed JNDI tree.

Binding a Remote Object

If you use an EmbeddableJNDIServer directly in your application, you can bind a remote object to it as such:

MyService stub = Hub.export(myService);
jndi.getLocalContext().bind("services/myService", stub);

If you connect to an EmbeddableJNDIServer remotely, do as follows (of course use the appropriate port):

Properties props = new Properties();
props.setProperty(InitialContext.PROVIDER_URL, "ubik://localhost:1099/");
props.setProperty(InitialContext.INITIAL_CONTEXT_FACTORY, RemoteInitialContextFactory.class.getName());
InitialContext ctx = new InitialContext(props);
MyService stub = Hub.export(myService);
ctx.bind("services/myService", stub);

Looking up a Remote Object

If you use an EmbeddableJNDIServer directly in your application, you can bind a remote object to it as such:

MyService stub = (MyService) jndi.getLocalContext().lookup("services/myService");

If you connect to an JNDI server remotely, do as follows (of course use the appropriate port):

Properties props = new Properties();
props.setProperty(InitialContext.PROVIDER_URL, "ubik://10.10.10.11:1099/");
props.setProperty(InitialContext.INITIAL_CONTEXT_FACTORY, RemoteInitialContextFactory.class.getName());
InitialContext ctx = new InitialContext(props);
MyService stub = (MyService) ctx.lookup("services/myService");

Resilient Remote Lookup

When connecting to a JNDI server remotely, the address of the host (and/or its port) may be misconfigured, or the host to which you're attempting to connect may be down. In this context, it is possible to help the org.sapia.ubik.rmi.naming.remote.RemoteInitialContextFactory connect to another existing JNDI server. To that end, you must specify the ubik.jndi.domain property, either as a system property, or as a property you pass to the InitialContext. The above example sets that property (by using the corresponding constant of the RemoteInitialContextFactory):

Properties props = new Properties();
props.setProperty(RemoteInitialContextFactory.UBIK_DOMAIN_NAME, "default");
props.setProperty(InitialContext.PROVIDER_URL, "ubik://localhost:1098");
props.setProperty(InitialContext.INITIAL_CONTEXT_FACTORY, RemoteInitialContextFactory.class.getName());
InitialContext ctx = new InitialContext(props);
MyService stub = (MyService) ctx.lookup("services/myService");

Internally, the RemoteInitialContextFactory instance will use an EventChannel to discover an existing JNDI server on the network.

Discovery

In order to discover new JNDI servers appearing on the network, or new services (i.e.: Ubik stubs) being bound to those servers, one can use the convenient org.sapia.ubik.rmi.naming.remote.discovery.DiscoveryHelper class. An instance if this class allows registering org.sapia.ubik.rmi.naming.remote.discovery.ServiceDiscoListeners and org.sapia.ubik.rmi.naming.remote.discovery.JndiDiscoListeners. These are notified when new services or JNDI servers appear, respectively. The following snippet provides a good illustration:

DiscoveryHelper discoHelper = new DiscoveryHelper(eventChannel.getReference());

discoHelper.addJndiDiscoListener(new JndiDiscoListener() {
  public void onJndiDiscovered(javax.naming.Context jndiContext) {
    log.debug("Binding service to JNDI");
    jndiContext.bind("/my/service", myService);
  }
});

discoHelper.addServiceDiscoListener(new ServiceDiscoListener() {
  public void onServiceDiscovered(ServiceDiscoveryEvent event) {
    log.debug("Got service: {}", event.getName());
    service = (MyService) event.getService();
  }
});

The type of code above may be used when a) a JNDI server might not yet be present on the network; or b) when a service might not yet be present on the network.

In the first case, the application's code holds logic to perform late discovery of Ubik JNDI nodes: the example code shows that the application binds a stub to the JNDI upon such a discovery occurring. In the second case, the code acquires the service (in fact a Ubik stub) that was discovered and assigns it to a member variable.

You'll see in the next section that Ubik uses a DiscoveryHelper to implement so-called "lazy stubs".

Lazy Stubs

Although the ability to discover remote objects is available to you, it might not be so convenient yet: it may be cumbersome to make application code "conscious" of services not yet being available at startup. See this for example:

private WorkService service;

@PostConstruct
public void init() throws NamingException {
  try {
    service = doLookup();
  } catch (NameNotFoundException e) {
    doRegisterForServiceDiscovery();
  }
}

public void performWork(Work someWork) {
  if (service == null) {
    throw new IllegalStateException("WorkService not yet available");
  }
  service.perform(someWork);
}

The above ilustrates cases where application code must be aware of the unavailability of distributed dependencies, and takes steps to work around those explicitely, in code.

An alternative to the above is to use the org.sapia.ubik.rmi.naming.remote.LazyStubInvocationHandler class: it implements the logic for discovering the desired remote dependency lazily. You may then just create an instance of it, as needed, and wrap it in a dynamic proxy implementing the interface of the dependency that you wish to use:

@Inject
private EmbeddableJNDIServer jndi;

@Inject
private DiscoveryHelper discoHelper;

private WorkService service;

@PostConstruct
public void init() throws NamingException {
  try {
    service = (WorkService) jndi.getLocalContext().lookup("/services/workservice");
  } catch (NameNotFoundException e) {
    LazyStubInvocationHandler handler = LazyStubInvocationHandler.Builder.newInstance()
      .context(jndi.getRemoteContext())
      .name("/services/workservice")
      .matchFunction(new Func<Void, LazyStubInvocationHandler>() {
        @Override
        public Void call(LazyStubInvocationHandler registeredHandler) {
          discoHelper.removeServiceDiscoListener(registeredHandler);
          return null;
        }
      })
      .build();
      
    // IMPORTANT: do not forget to register the handler with the DiscoveryHelper
    discoHelper.addServiceDiscoListener(handler);
    service = (WorkService) Proxy.newProxyInstance(
      Thread.currentThread().getContextClassLoader(), 
      new Class<?>[] {WorkService.class}, 
      handler
    );
  }
}

public void performWork(Work someWork) {
  service.perform(someWork);
}

The advantage in this solution is that the service variable is never null: it is assigned the stub that was found in the JNDI tree, or with one which will attempt lazy lookup of the service.

Let's review what the above does:

  1. An attempt is made to look up the WorkService.
  2. If it is not found, a LazyStubInvocationHandler is created which wraps:
    • The RemoteContext of the JNDI server.
    • The name of the service that is expected.
    • A Func instance that unregisters the LazyStubInvocationHandler from the DiscoveryHelper after the remote stub is discovered (see the next point about the actual registration with the DiscoveryHelper).
  3. The LazyStubInvocationHandler is then registered with the DiscoveryHelper, in order to receive the remote stub that was initially expected.
  4. The proxy implementing the servide interface is created for wrapping the LazyStubInvocationHandler.

While your lazy stub has not yet found the remote object it expects, it will throw an exception every time one of its methods is called. This should be a temporary state, and ideally you would plan your deployments so that service dependencies are resolved in an orderly manner.

Miscellaneous

This section covers various relevant subjects pertaining to using Ubik, in non-specific order.

StubContainer

Since the JNDI server could be started on a separate machines then those of your application, and services bound to it in a remote manner, it may well not have your classes in its class path. For that reason, upon binding, your remote objects are converted to a "neutral" form, that is: as instances of the org.sapia.ubik.rmi.server.stub.StubContainer interface.

When you look up a stub remotely, using Ubik's client-side implementation of the JNDI (that is, when you use the RemoteInitialContextFactory), that implementation does the conversion to stub automatically, upon lookup.

When you rather use the EmbeddableJNDIServer directly in application code, be aware that:

  • When you invoke the getLocalContext() method, it indeed returns a Context implementation that converts to stub automatically what is looked up from the JNDI tree;
  • When you invoke the getRemoteContext() method, it returns a Context implementation that returns StubContainer instances.

From a StubContainer, you can obtain the org.sapia.ubik.rmi.server.stub.StubInvocationHandler that it wraps. But what you would rather do is obtain the Ubik stub to which it corresponds:

WorkService service = stubContainer.toStub(Thread.currentThread().getContextClassLoader());

Health Checks

It is a good practice, as part of your applications, to asynchronously check connections to external systems and report such status in an administration console, or through some HTTP endpoint.

All Ubik stubs (which are, as you should know by now, dynamic proxies) wrap an instance of the org.sapia.ubik.rmi.server.stub.StubInvocationHandler interface. The interface specifies the following method:

public boolean isValid() throws RemoteException;

You can invoke that method as part of health check logic, in your code. See the following:

private WorkService service;

public boolean isUp() {
  
  try {
    return Stubs.getStubInvocationHandler(service).isValid();
  } catch (RemoteException e) {
    return false;
  }

}

In the above case, we're obtaining the StubInvocationHandler that's wrapped by the stub, calling its isValid() method. A RemoteException may be thrown: this must be interpreted as the server being unavailable.