Thursday, October 14, 2010

Implementing Singleton in cluster envirnoment - Option 3

Clustering and RMI Singletons

Clustering is when you have J2EE containers that are running on different VMs talk to each other. Clustering is used to provide load balancing and fail over for J2EE clients.
The simple/local Singleton as shown is a non-distributed object. Therefore in a clustered environment you will end up with at least one Singleton object on each server. This of course may be ok for the design requirements.
However if the design is to have one Singleton for the cluster then a common approach is to implement a "pinned service". This refers to an RMI object that is only located on one container in the cluster. Its stub is then registered on the clustered JNDI tree making the object available cluster wide. This raises of causes one issue, what happens when the server containing the RMI Singleton crashes?
A Container in the cluster could try to bind a new RMI Singleton if it notices it is missing out of the JNDI tree. However this could cause issues if all the containers try to bind new RMI Singletons at the same time in response to a failure.
Generally at the end of the day RMI Singletons do tend to have the drawback that they end up as single points of failure.
In the following code example a local Singleton is used to act as a Wrapper around a RMI object that is bound into the clusters JNDI tree.
import javax.naming.*;
import java.rmi.*;

public class RMISingletonWrapper {
  private static RMISingletonWrapper instance = null;
  private static String SINGLETON_JNDI_NAME = "RMISingleton";

  public static RMISingletonWrapper getInstance() {
    return instance;
  }

  // All methods in delegate the method call to the actual
  // Singleton that lives on the clustered JNDI tree.
  public void delegate() {
    try {
      RMISingleton singleton = getRMISingleton();
      singleton.delegate();
    } catch (Exception e) {
      // Could try and recover
      e.printStackTrace();
    }
  }

  // Locate the true Singleton object in the cluster.
  private RMISingleton getRMISingleton() {
    RMISingleton rmiSingleton = null;
    try {
      Context jndiContext = new InitialContext();
      Object obj = jndiContext.lookup(SINGLETON_JNDI_NAME);
      rmiSingleton = (RMISingleton)PortableRemoteObject.narrow(
        obj,
        Class.forName("examples.singleton.rmi.RMISingleton"));
    } catch (Exception e) {
      // Could try and recover
      e.printStackTrace();
    }
    return rmiSingleton;
  }
}

Distributed Singleton Caches

One of the most common usages of Singletons is as caches of data. This use has issue for non RMI Singletons in a clustered environment. Problems happen when you attempt to do an update to the cache. Since a Singleton instance exists on each Container any update to the cached data by one Singleton will not be replicated to the other Singletons that exist on the other Containers.
This issue can be resolved by the use of the Java Messaging API to send update messages between Containers. In this approach if an update is made to the cache on one Container a message is published to a JMS Topic. Each Container has a listener that subscribes to that topic and updates its Singleton cache based on the messages it receives. This approach is still difficult as you have to make sure that the updates received on each container are handled in a synchronous fashion. JMS messages also take time to process so the caches may spend some time out of sync.
In the following simplistic implementation of a distributed Cache a CacheManager Singleton holds a Map of cached items. Items to be cached are placed in a CachItem object which implements the ICacheItem interface.
The CacheManager does not make any attempt to remove old items from the Cache based on any criteria like "Last Accessed Time".
import javax.jms.*;

public class CacheManager implements MessageListener {
  public static CacheManager instance = null;
  public static Map cache = new HashMap();

  private TopicConnectionFactory topicConnectionFactory;
  private TopicConnection topicConnection;
  private TopicSession topicSession;
  private Topic topic;
  private TopicSubscriber topicSubscriber;
  private TopicPublisher topicPublisher;

  private final static String CONNECTION_FACTORY_JNDI_NAME =
    "ConnectionFactory";
  private final static String TOPIC_NAME = "TopicName";

  public static void initInstance() {
    instance = new CacheManager();
  }

  public static CacheManager getInstance() {
    return instance;
  }

  public synchronized void addCacheItem(ICacheItem cacheItem) {
    CacheMessage cacheMessage = new CacheMessage();
    cache.put(cacheItem.getId(), cacheItem.getData());
    cacheMessage.setMessageType(CacheMessage.ADD);
    cacheMessage.setCacheItem(cacheItem);
    sendMessage(cacheMessage);
  }

  public synchronized void modifyCacheItem(ICacheItem cacheItem) {
    CacheMessage cacheMessage = new CacheMessage();
    cache.put(cacheItem.getId(), cacheItem.getData());
    cacheMessage.setMessageType(CacheMessage.MODIFY);
    cacheMessage.setCacheItem(cacheItem);
    sendMessage(cacheMessage);
  }

  public ICacheItem getCacheItem(String key) {
    return (ICacheItem)cache.get(key);
  }

  private CacheManager() {
    try {
      InitialContext context = new InitialContext();
      topicConnectionFactory = (TopicConnectionFactory)
        context.lookup(CONNECTION_FACTORY_JNDI_NAME);
      topicConnection = topicConnectionFactory.createTopicConnection();
      topicSession = topicConnection.createTopicSession(
        false, Session.AUTO_ACKNOWLEDGE);
      topic = (Topic) context.lookup(TOPIC_NAME);
      topicSubscriber = topicSession.createSubscriber(topic);
      topicSubscriber.setMessageListener(this);
      topicPublisher = topicSession.createPublisher(topic);
      topicConnection.start();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public void onMessage(Message message) {
    try {
      if (message instanceof ObjectMessage)  {
        ObjectMessage om = (ObjectMessage)message;
        CacheMessage cacheMessage = (CacheMessage)om.getObject();
        ICacheItem item =  cacheMessage.getCacheItem();
        interpretCacheMessage(cacheMessage);
      }
    } catch (JMSException jmse) {
      jmse.printStackTrace();
    }
  }

  private void interpretCacheMessage(CacheMessage cacheMessage) {
    ICacheItem cacheItem = cacheMessage.getCacheItem();
    if (cacheMessage.getMessageType()==CacheMessage.ADD) {
      synchronized (this) {
        cache.put(cacheItem.getId(), cacheItem.getData());
      }
    } else if (cacheMessage.getMessageType()==CacheMessage.MODIFY) {
      synchronized (this) {
        cache.put(cacheItem.getId(), cacheItem.getData());
      }
    }
  }

  private void sendMessage(CacheMessage cacheMessage) {
    try {
      Message message = topicSession.createObjectMessage(cacheMessage);
      topicPublisher.publish(message);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

Class Loading

Containers tend to implement their own class loading structures to support hot deployment for J2EE components and class isolation WAR files.
Class isolation in WAR files means that all classes found in a WAR file must be isolated from other deployed WAR files. Each WAR file therefore is loaded by a separate instance of the Class loader. The purpose is to allow each WAR file have its own version of commonly named JSPs like "index.jsp".
If a Singleton class is located in several WAR files it will mean that a separate Singleton instance will be created for each WAR file. This may of course be ok for the required design but it is worth being aware of.




Resource --

http://www.roseindia.net/javatutorials/J2EE_singleton_pattern.shtml

No comments:

Post a Comment