Manage distributed sessions

To avoid a single point of failure, use a distributed architecture for managing sessions

For a good description of what sessions are and what the problem with having sessions across multiple servlet servers is, refer to Thomas E. Davis and Craig Walker’s “Take Control of the Servlet Environment, Part 2: Alternatives to Servlet Session Management” (JavaWorld, December 21, 2000). Basically, the problem is that if you have more than one servlet server, session information exists only in one servlet engine’s JVM and is not communicated to the other servlet servers. If a servlet server fails or shuts down for maintenance, any information saved in a session is lost. Also, if an environment has multiple servlet servers, a user with a session must always be redirected to the same servlet server in order to reference any data in that session. Davis and Walker suggest using a relational database that all servlet servers can access to save the session information. However, that solution still has a single point of failure: the server hosting the database. If the database went down, all session information would become unavailable to the servlet servers. Also, saving serialized objects into the database is a functionality that is difficult to implement in all databases.

Another possibility for multiserver session management utilizes the JavaSpaces API (for documentation, see Resources) to maintain records of the session objects. However, if the machine hosting the JavaSpace were to go down because of crashing or maintenance, all session information would be lost. So again, we are left with a single point of failure.

To accomplish the distributed session server with n-nodes, we must address three main problems:

  1. How to set up a repository to store session information
  2. How to synchronize distributed repositories
  3. How to let a servlet server retrieve the session information in such a way that if one repository suddenly goes offline, it tries to retrieve the object from the next repository

Introduction to the Mnemosyne

The repository we will use to store the session information is an implementation of the Mnemosyne interface (named after the Greek goddess of memory and mother of the muses). An object that implements Mnemosyne is responsible for managing all objects in the repository. Any other object that wants to write, retrieve, or remove objects in the repository must call a method of the Mnemosyne.

To be saved to a Mnemosyne, an object must implement the Memory interface, which defines the equalsMemory()operation for determining whether two memory objects are equivalent. That allows the Mnemosyneto figure out what object should be returned on a read or a take request. The Memory interface also extends Serializable so that you can use RMI to transmit the object across the network.

A Mnemosyne uses three other interfaces to represent its state:

  1. A CommonContext interface will store all information for the Mnemosyne. Each Mnemosynewill have one instance of a CommonContextobject to allow for synchronization between methods when reading, writing, and taking Memoryobjects. For writing and taking, the CommonContextdefines both “silent” and “loud” methods. The silent methods are used when objects need to be added without any event notifications. For example, when a Mnemosynereceives a WriteRemoteEvent (a notification that an object has been written to a remote Mnemosyne), it will want to write another object to the CommonContext. However, it doesn’t need to notify the other remote Mnemosynes; the original Mnemosynehas notified them. So the write is done “silently” by calling the CommonContext‘s silentWrite()method. The “loud” methods give details of the event to any interested listeners that are called when an object is first put into the context.
  2. A Transaction interface allows for distributed transactions when reading, writing, or taking Memoryobjects. That means multistep operations can occur on the Mnemosyne.
  3. A TransactionContext interface manages a distributed transaction. That makes it possible to abort or commit transactions.

Keeping Mnemosynes synchronized is accomplished with two methods defined by the Mnemosyne: synchronize()and notify(). synchronize()is intended to get a local Mnemosyne“in sync” with a Vector of other Mnemosynes. (Those Mnemosynes may be local or remote, but for the sake of clarity, we will assume they are remote.) A sample implementation of the synchronize()method (from the MnemosyneImplclass) is displayed below. (See Resources for the complete sample code to this article.)

public void synchronize(Vector Mnemosynes)
                throws RemoteException, TransactionException
    {
        // ...
                            // The MatchAllMemory object is a utility class that
returns true when 
                           //  compared against any Memory object
            MatchAllMemory matchAllMemory = new MatchAllMemory();
            // Obtain all Memory's from the Primary
            Mnemosyne Mnemosyne = (Mnemosyne) Mnemosynes.firstElement();
            Vector allMemories = Mnemosyne.readAll(matchAllMemory,null);
            // Write all Memory's silently as there is no need
                            //  to notify write listeners
            commonContext.silentWriteAll(allMemories);
            // Register to send and receive events
            Enumeration enumeration = Mnemosynes.elements();
            while(enumeration.hasMoreElements())
            {
                Mnemosyne nextMnemosyne = (Mnemosyne) enumeration.nextElement();
                // Register to obtain notification
                nextMnemosyne.addTakeRemoteEventListener(this, matchAllMemory); 
                nextMnemosyne.addWriteRemoteEventListener(this, matchAllMemory);
                // Register to send notification
                addTakeRemoteEventListener(nextMnemosyne, matchAllMemory);
                addWriteRemoteEventListener(nextMnemosyne, matchAllMemory);
            }
         // ...
    }

The local Mnemosyne object reads all of its Memoryobjects of the first Mnemosyne in the Vector, and silently writes them to its CommonContextobject. Next, the local Mnemosyneadds itself to all remote Mnemosynes, as both a TakeRemoteEventListenerand a WriteRemoteListener. That means that any takes or reads on the remote Mnemosynes will result in a call to the local Mnemosyne‘s notify() method. Finally, the local Mnemosyne adds the remote Mnemosyneto its list of TakeRemoteEventListeners and WriteRemoteListeners. This ensures that any write or take calls will notify the remote Mnemosyne.

Now our synchronized local Mnemosyne object needs to update all other Mnemosynes whenever a Memoryobject is added or removed. You can accomplish that with the notify() method. Whenever a write or take event occurs, a Mnemosyne calls the notify()method of all appropriate listeners for the event. In the synchronize() method, you register the local Mnemosyne as a listener for take and write events on all remote Mnemosynes. If such an event occurs on any of those remote Mnemosynes, the local Mnemosyne‘s notify() method will be called. When that happens, the local Mnemosyne must respond to the event. Below is an example of how the Mnemosynecan synchronize itself with the remote Mnemosyne:

public void notify(RemoteEvent remoteEvent) throws RemoteException
    {
        // Write the written Memory to self, but no need to notify all of the 
        //  Mnemosynes 
        if(remoteEvent instanceof WriteRemoteEvent)
        {
            WriteRemoteEvent wre = (WriteRemoteEvent) remoteEvent;
            commonContext.silentWrite(wre.getMemory());
        }
        // Take the written Memory from self, but no need to notify all of the 
        //  Mnemosynes
        if(remoteEvent instanceof TakeRemoteEvent)
        {
            TakeRemoteEvent tre = (TakeRemoteEvent) remoteEvent;
            commonContext.silentTake(tre.getMemory());
        }
   }

A Mnemosyne has now been set up that can hold memory objects, synchronize itself with remote Mnemosynes, and keep itself up to date if any remote Mnemosynes gain or lose a Memory object.

To manage HTTP sessions using Mnemosynes, a servlet creates an instance of HttpSession (using the getSession()method from HttpServletRequest), wraps the session in a class that implements Memory (in this example, it is called SessionWrapper), and writes the wrapper class to a Mnemosyneby calling the Mnemosyne object’s write() method.

By calling the write() method, the Memoryobject that wraps the session is serialized, sent down the network to the Mnemosyne, and instantiated on the remote machine. When the Memory object is written to the Mnemosyne, a WriteRemoteEventis sent to all WriteRemoteEventListeners registered with the Mnemosyne. This allows all other Mnemosynes to add the new object to their repository as Mnemosynes.

To look up a stored session, a servlet calls the read()method to look for the Memory object that contains the session. If the Mnemosyne finds the object, the object is sent via RMI back to the servlet server.

Finally, to remove the session, the servlet would call the Mnemosyne's take() method. The Mnemosynewould send back the Memory object just like it does on the read, but the object would also be removed from the repository. Also, a TakeRemoteEvent would be sent out to all TakeRemoteEventListeners. That would notify all remote Mnemosynes of the Memoryobject’s removal.

Setting up a session server

Now that we have shown how the repository of objects will be maintained on multiple servers, we will show you how to build an implementation of a session server. On initialization, a session server does the following:

  1. Creates the local Mnemosyne object
  2. Binds the local Mnemosyne to the RMI registry
  3. Synchronizes the local Mnemosyne object with all other remote Mnemosynes

First, an instance of the Mnemosyne interface is obtained from the factory and is bound to the local IP for the session server. (Refer to the SessionServer object for a full implementation.)

protected void bindMnemosyne()
    {
        //...
        
        // Get Mnemosyne 
        Mnemosyne Mnemosyne = null;
        try
        {
            Mnemosyne = MnemosyneFactory.getMnemosyne();
        }
        catch(RemoteException remoteException)
        {
            System.out.println("Internal error:");
            System.out.println("Can't create a Mnemosyne");
            System.exit(1);
        }
        // Bind the Mnemosyne to MnemosyneImpl
        try
        {
            String rmiURL = "//" + _localIP + "/MnemosyneImpl";
            Naming.rebind(rmiURL, Mnemosyne);
        }
        catch(ArrayIndexOutOfBoundsException ArrayIndexOutOfBoundsException)
        {
            throw new IllegalArgumentException("LocalIP is invalid");
        }
        catch(MalformedURLException malformedURLException)
        {
            throw new IllegalArgumentException("LocalIP is invalid");
        }
        catch(RemoteException remoteException)
        {
            System.out.println("Internal error:");
            System.out.println("Can't rebind a Mnemosyne to MnemosyneImpl");
            System.exit(1);
        }
         
        // ...
    }

The synchronization can occur by giving the local Mnemosynea list of URLs that represent the RMI naming references to the remote session servers. Those URLs are stored in an array of Strings called rmiURLs. In the reference implementation shown in the SessionServerclass, the URLs come from the command line as parameters, but could be derived from any source:

protected void synchronizeMnemosyne()
    {
        // Obtain localMnemosyne
        Mnemosyne localMnemosyne = null;
        try
        {
            localMnemosyne = (Mnemosyne) Naming.lookup(_localIP);
        }
        catch(Exception exception)
        {
            System.out.println("Internal error:");
            System.out.println("Can't lookup local MnemosyneImpl");
            System.exit(1);
        }
        // Obtain remoteMnemosynes for synchronization
        Vector remoteMnemosynes = new Vector(); 
        // The _rmiURLS object is an array of Strings that represents
        // the remote servers that need to be synched with.
        for(int index = 1;index < _rmiURLS.length;index++)
        {
            try
            {
                remoteMnemosynes.add(Naming.lookup(_rmiURLS[index]));
            }
            catch(Exception exception)
            {
                // Assume the remote Mnemosyne is down and move on
            }
        }
        // Synchronize
        try
        {
            if(remoteMnemosynes.size() > 1)
             localMnemosyne.synchronize(remoteMnemosynes);
        }
        catch(Exception exception)
        {
            System.out.println("Internal error:");
            System.out.println("Can't synchronize local MnemosyneImpl");
            System.exit(1);
        }
    }

Retrieving the Mnemosyne remotely

Now, you set up the code on the servlet server to access a remote Mnemosyne. To load a Mnemosyne containing the session information without needing a specific server to be online, you create an instance of FailoverHandler, a utility class that uses the JDK 1.3’s Proxy API to handle session server crashes. The FailoverHandler takes an array of Strings as arguments that represent the RMI URLs to access the remote session servers. Next, a Mnemosyne instance is obtained from the Proxy class. The initializeMnemosyne()method in the SessionManagerclass shows how this is done:

public static void initializeMnemosyne(String[] rmiURLs) 
    {        
        // Setup the handler for failing over.
        FailoverHandler fh = new FailoverHandler(null, rmiURLs);
        
        // Get an instance of the Mnemosyne.
        _Mnemosyne = 
            (Mnemosyne)Proxy.newProxyInstance(Mnemosyne.class.getClassLoader(),
                                              new Class[] { Mnemosyne.class },
                                              fh );        
    }

If you use the Proxy class to obtain the instance of the Mnemosyne, all method calls must pass through the FailoverHandler's invoke() method. When a method is called on the Mnemosyne, the FailoverHandlerwill try to call the method on a remote object. If the method cannot be called (for example, if the server has gone down), the FailoverHandler gets the next delegate from the list of URLs supplied to the constructor. This allows a seamless switching to the next active session server:

// Set up the URLs for remotely loading classes
    public FailoverHandler(Remote delegate, String[] delegateURLS)
    {
        this.delegateURLS = delegateURLS;
        
        // If the delegate comes in as null, we just want to get the next
        // valid delegate.
        try {
            this.delegate = 
                ((delegate == null)?getNextValidDelegate():delegate);
        } catch (RemoteException ex) {
            // If a remote exception occurs, the delegate urls passed
            // in must not be valid.  Throw an IllegalArgumentException
            // to notify the caller of this.
            throw new IllegalArgumentException("Remote URLs could not "
                                               + "be found");
        }
        
    }
   
    public Object invoke(Object proxy, 
                         Method method, 
                         Object[] arguments) 
                  throws Throwable
    {
        // Delegate the method call
        while(true)
        {
            try
            {
                // try to invoke the called method on the last delegate obtained
                return method.invoke(delegate, arguments);
            }
            catch(InvocationTargetException invocationTargetException)
            {
        // if the target could not be invoked, try to get the next delegate
                try
                {
                    throw invocationTargetException.getTargetException();
                }
                catch(RemoteException remoteException)
                {
                    delegate = getNextValidDelegate();
                }
            }
        }
    }
   // get the next delegate from the URL list supplied in the constructor
   protected Remote getNextValidDelegate() throws RemoteException 
    {
        // Loop until done
        for(int i = 0; i < delegateURLS.length;i++)
        {
            try
            {
                return Naming.lookup(delegateURLS[i]);
            }
            catch(Exception exception)
            {
            }
        } 
        // Throw, as all lookups failed
        throw new RemoteException("All lookup failed");
    }

When using a FailoverHandler object, the transition from one session server to another is invisible to any client that is referencing a Mnemosyne. Now that we can access the remote session servers and are insulated from one failing, we must create a wrapper object for our HttpSession. The SessionWrapperdescribes such a class; however, it assumes that the HttpSession implementation is also serializable (like the Apache JServ implementation, which we use in this article). If it is not serializable, you can easily modify the wrapper to move session values into a Hashtable and retain other information (id, creation time, and so forth) in other member variables.

public interface SessionWrapper extends Memory
{ 
    /**
     *  Gets the HttpSession information. 
     */
    public HttpSession getSession();
}
public class SessionWrapperImpl implements SessionWrapper
{ 
    
    /** Key to identify the session. */
    protected String _id;
    /** The current HttpSession information. */
    protected HttpSession _sess;
    /**
     *  Sets the id up, but does not set up
     *  the session information.  This can be used to 
     *  look for a session using a read.
     */
    public SessionWrapper(String id) { 
        _id = id;
    }
    /**
     *  Sets up a SessionWrapper with a session.  
     *  The id of the session wrapper is set by the
     *  id of the session.
     */
    public SessionWrapper(HttpSession sess) {
        _sess = sess;
        _id = sess.getId();
    }
    /** 
     *  This method will return true if the Memory object is an 
     *  instance of SessionWrapper, if the current SessionWrapper
     *  has set up an ID, and if the id of the SessionWrapper we 
     *  are comparing it to is the same.
     */
    public boolean equalsMemory(Memory m) {
        // See if the 
        return (m instanceof SessionWrapper 
                && _id != null
                && _id.equals(((SessionWrapper)m)._id)); 
    }
    /**
     *  Gets the HttpSession information. 
     */
    public HttpSession getSession() {
        return _sess;
    }
}

The SessionWrapper class implements the Memoryinterface so the HttpSession object’s id can compare remote sessions.

The final step is to create the read(), write(), and delete() methods to manage the session remotely. To do this, three static methods are added to the SessionManager class:

/**
     *  Gets the HttpSession information from the Mnemosyne that has
     *  been created in initialization. 
     */
    public static HttpSession getSession(String id) 
        throws RemoteException
    {
        try {
            SessionWrapper result 
                = (SessionWrapper)_Mnemosyne.read(new SessionWrapper(id), 
                                                  null);
            return result.getSession();
        } catch (TransactionException ex) {
            // No transaction exception should be thrown since
            // we are not doing a transaction.
            ex.printStackTrace();
        }
        return null;
    }
    
    /**
     *  Saves a session to the Mnemosyne specified in the initialization.
     */
    public static void saveSession(HttpSession sess) 
        throws RemoteException
    {
        try {
            _Mnemosyne.write(new SessionWrapper(sess), null);
        } catch (TransactionException ex) {
            // No transaction exception should be thrown since
            // we are not doing a transaction.
            ex.printStackTrace();
        }
    }
    /**
     * Removes a session from the Mnemosyne specified in the initialization.
     */
    public static void removeSession(String id) 
        throws RemoteException
    {
        try {
            _Mnemosyne.take(new SessionWrapper(id), null);
        } catch (TransactionException ex) {
            // No transaction exception should be thrown since
            // we are not doing a transaction.
            ex.printStackTrace();
        }
    }

From a servlet, sessions can be managed as follows:

public void init(ServletConfig conf) throws ServletException {
         // call a method to get a list of RMI URLs describing where the session
servers are.  
        // For example: //server1.foo.com/MnemosyneImpl,
//server2.foo.com/MnemosyneImpl, etc.
                    String[] urls = getURLs(conf);    // Method to get the URLs
from properties for the session servers
                       SessionManager.initializeMnemosyne(urls)
}
public void doGet(HttpServletRequest req, HttpServletResponse rsp) throws
IOException {
                // ...
                // Get the session saved in the cookie.  This is just for the ID
                HttpSession baseSess = req.getSession()
                // Based on the session id, get the real session from the
Mnemosyne
HttpSession realSess = SessionManager.getSession(base.getId());
                // .. Do something with the session
                // Save something from the session.
                SessionManager.saveSession(realSess);
}

Conclusion

While this article gives an example of distributed session management, you could use these techniques to manage any distributed “memory management” that must be tolerant of a node failure. Mnemosyne could also be used in a peer-to-peer application that has members constantly joining and leaving. Using a Mnemosyne implementation could allow new members to be quickly synchronized into a group’s actions, and would not require that any node stay active to keep the system functioning.

Kelly Davisgraduated from MIT and almost
finished a Ph.D. from Rutgers in physics. After graduate school, he
began to develop server-side Java applications, concentrating on
multithreaded servlet, EJB, and JNI applications. In addition to
his Java experience, Kelly is very interested in the development of
neural networks. Kelly now lives in Germany.

Rob Di Marcograduated from the University of
Virginia with degrees in chemistry and physics. On a hunch that
this whole computer programming thing might not be just a fad, he
plunged into Java development right after graduation. Rob works as
a software architect for the Adrenaline Group, mainly writing
server-side Java applications, and is a Sun-certified Java
programmer, developer, and architect.

Source: www.infoworld.com