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:
- How to set up a repository to store session information
- How to synchronize distributed repositories
- 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 Mnemosyne
to 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:
- A
CommonContext
interface will store all information for theMnemosyne
. EachMnemosyne
will have one instance of aCommonContext
object to allow for synchronization between methods when reading, writing, and takingMemory
objects. For writing and taking, theCommonContext
defines both “silent” and “loud” methods. The silent methods are used when objects need to be added without any event notifications. For example, when aMnemosyne
receives aWriteRemoteEvent
(a notification that an object has been written to a remoteMnemosyne
), it will want to write another object to theCommonContext
. However, it doesn’t need to notify the other remoteMnemosyne
s; the originalMnemosyne
has notified them. So the write is done “silently” by calling theCommonContext
‘ssilentWrite()
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. - A
Transaction
interface allows for distributed transactions when reading, writing, or takingMemory
objects. That means multistep operations can occur on theMnemosyne
. - A
TransactionContext
interface manages a distributed transaction. That makes it possible to abort or commit transactions.
Keeping Mnemosyne
s 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 Mnemosyne
s. (Those Mnemosyne
s 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 MnemosyneImpl
class) 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 Memory
objects of the first Mnemosyne
in the Vector
, and silently writes them to its CommonContext
object. Next, the local Mnemosyne
adds itself to all remote Mnemosyne
s, as both a TakeRemoteEventListener
and a WriteRemoteListener
. That means that any takes or reads on the remote Mnemosyne
s will result in a call to the local Mnemosyne
‘s notify()
method. Finally, the local Mnemosyne
adds the remote Mnemosyne
to its list of TakeRemoteEventListener
s and WriteRemoteListener
s. This ensures that any write or take calls will notify the remote Mnemosyne
.
Now our synchronized local Mnemosyne
object needs to update all other Mnemosyne
s whenever a Memory
object 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 Mnemosyne
s. If such an event occurs on any of those remote Mnemosyne
s, 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 Mnemosyne
can 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 Mnemosyne
s, and keep itself up to date if any remote Mnemosyne
s gain or lose a Memory
object.
To manage HTTP sessions using Mnemosyne
s, 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 Mnemosyne
by calling the Mnemosyne
object’s write()
method.
By calling the write()
method, the Memory
object 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 WriteRemoteEvent
is sent to all WriteRemoteEventListener
s registered with the Mnemosyne
. This allows all other Mnemosyne
s to add the new object to their repository as Mnemosyne
s.
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 Mnemosyne
would 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 TakeRemoteEventListener
s. That would notify all remote Mnemosyne
s of the Memory
object’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:
- Creates the local
Mnemosyne
object - Binds the local
Mnemosyne
to the RMI registry - Synchronizes the local
Mnemosyne
object with all other remoteMnemosyne
s
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 Mnemosyne
a 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 SessionServer
class, 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 SessionManager
class 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 FailoverHandler
will 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 SessionWrapper
describes 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 Memory
interface 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.