Taming the Stateless Beast: Managing Session State Across Servers on a Web FarmThis article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
This article assumes youï¿½re familiar with ASP, ADO, Visual Basic, SQL Server
|Level of Difficulty 1 2 3 |
Download the code for this article: Papa1000.exe (44KB)
Browse the code for this article at Code Center: Session Manager
SUMMARY Running a Web farm means managing session state across servers. Since session state can't be shared across a Web farm with Internet Information Services 5.0, a custom solution is required. One such solution using a tool called the session manager is described here.
The article begins with a description of the SQL Server database used to store state information, the stored procedures used to update it, and the retrieval of session data. ASP code drives the session manager tool and the COM and COM+ components that run the stored procedures.
hey call you a god. You can hear it in the halls and by the water cooler. In fact, the Web application you've implemented is so popular that you practically walk on water around your office. You're doing all the right things to keep up with the growing demand on your company intranet: you've beefed up your hardware, written all of your components to run under COM+, and have created some pretty cool Web pages to boot.
Inevitably, there comes a point when you simply need to move to a Web farm. But have you thought about how to manage state in a multiserver environment? After all, you're getting used to all of the hero worship, aren't you?
Before I get into the guts of this article, I'll give you some background on the problem that I'm trying to solve. Then I'll discuss one viable solution for my state management needs. At that point, I'll jump into the code with some hardcore examples. And so you won't have to start from scratch, I've included the complete set of ASP files, Visual BasicÂ®-based and COM+ components (including source code), along with the SQL script for download (see the link at the top of this article). So without further delay, let's jump in and make sure you can retain your immortal stature.
Working the Farm
One of the problems with sessions in Internet Information Services (IIS), and indeed most servers, is that a session's scope is limited to a single Web server. In a Web farm environment, however, an HTTP request can be routed to any one of the servers in the Web farm. Here's a scenario that should shed some more light on the problem. Assume there are three servers in your Web farm. In the first trip to the Web application, the HTTP request gets routed to Web Server #1. Once there, some data (keeping state) is stored in that Web server's Session object. Then, another HTTP request occurs on the Web app; this request gets routed to Web Server #2. (You never know which server in the Web farm will handle a request.) If you look for the session data on this server, you won't find it. It is still over on Web Server #1. The problem is that you can't be guaranteed that a request will be routed to the same Web server each time, nor can the session state be shared across the Web farm using IIS 5.0.
So what do you do? Does this mean you have to forego state altogether in Web applications? (Obviously the answer is no, otherwise this would be a short article.) A couple of choices include moving to another product or developing your own state management software. Or you could sit back and wait to see if future versions of IIS will provide a mechanism to handle state on a Web farm. Well, I don't like waiting and I wasn't ready to give up IIS for this one problem, so I fought back and developed my own state management tool.
The tool I designed isn't earth-shattering by any means. In fact, it is pretty basic. The idea is that all the state is stored in a central database instead of in memory on each Web server. This state information is contained within a simple database structure and can be accessed via a COM object written in Visual Basic that runs under COM+. After all, this Session Manager tool needs to handle several concurrent users, so it's important to do everything possible to keep it scalable. Running it under COM+ properly, using stored procedures, and keeping the transactions short and sweet will aid in that effort.
Under the Hood
The best place to start is at the foundation of the Session Manager: the database (see Figure 1
). While this database design isn't going to win you any awards, it serves its purpose nicely. I chose to use SQL Serverâ¢, but you could use any other enterprise database. Here, I'm keeping it simple by creating a table (tblSession) to store the session key that each user gets. Then table tblSessionData stores the individual session variables and their corresponding values.Figure 1 The Session Managerï¿½s Database
The sSessionKey field is defined with the SQL Server UNIQUEIDENTIFIER data type. This indicates that the sSessionKey field contains a GUID, which is guaranteed to be unique even across multiple servers. This makes it perfect for identifying a specific user who hits my Web application. For each user who hits the site, a GUID is generated and stored in the sSessionKey field. Then this session key is stored on the user's machine using a cookie, but I'll get to that subject later.
The tblSession table has two dates. The first date/time field, dtLastAccessDateTime, is updated every time a user visits my Web application. This field can be used to track how long it has been since the user has visited my site and to time them out if necessary. The other date/time field, dtCreatedDateTime, simply tracks when the session was first initiated.
The table tblSessionData pairs a valid session key from tblSession with a session variable (sVariableName). An example of a variable name might be UserID or Password. Together, the session key and the session variable name identify a single session variable value (sValue). For example, a user could have an entry for their session key (in the sSessionKey field), their UserID (in the sVariableName field), and the actual UserID such as JPapa (in the sValue field). In this case, tblSession gets a GUID such as this:
The tblSessionData entry for each session variable might look like the following:
|SessionKey ||sVariableName ||sValue|
|EDC62F1A-1F8D-11D4-8495-00C04F4C354C ||sPassword ||elmo|
|EDC62F1A-1F8D-11D4-8495-00C04F4C354C ||sUserID ||Haley |
Managing Sessions through Stored Procedures
But how is the GUID created in the first place? Calling the prAPP_Insert_Session stored procedure uses the SQL Server NEWID function to generate a unique GUID that is inserted into the table tblSession:
CREATE PROCEDURE prAPP_Insert_Session
SET NOCOUNT ON
DECLARE @sKey UNIQUEIDENTIFIER
SET @sKey = NEWID()
INSERT tblSession (sSessionKey, dtLastAccessDateTime) VALUES
The tool provides stored procedures to delete sessions, update session values, insert new session variables, and (of course) retrieve session data. Figure 2
shows the prAPP_Get_SessionValues stored procedure, which retrieves all of the session variables and values for a specific session key. Calling this stored procedure with the session key returns the UserID Haley and the Password elmo. It also calls the prAPP_Update_Session stored procedure (shown in Figure 3
) to update the last access time for the session.
The other stored procedures are equally important, as they are used to manipulate the session data as needed. They include:
- CREATE PROCEDURE prAPP_Delete_Sessions
- CREATE PROCEDURE prAPP_Insert_SessionVariable
- CREATE PROCEDURE prAPP_Update_SessionVariable
- CREATE PROCEDURE prAPP_Get_SessionValue
- CREATE PROCEDURE prAPP_Exist_Session
Download the StoredProcs.sql file, from the MSDNÂ® Magazine Web site, to see these procedures implemented.
Running Stored Procedures
Now that the database has been designed and is ready to go, the tool needs a way to run the stored procedures to manage my sessions. I chose to use Visual Basic to create a COM component that runs under COM+. The component contains two classes and a common module for some generic functions that I'll need. (In the ASP code included in the download for this article, I call this component Blue_SessionManager.)
The first class is the QSession class, which is responsible for making all of the session retrieval calls (strictly queries, thus the Q prefix). QSession's COM+ transaction mode is defined as "No Transactions", so it never participates in a COM+ transaction, but it will run under COM+. Since the QSession class will never alter data directly, I don't need to worry about spanning transactions, so by not creating a transaction in the first place, the overhead will be minimized. The following is a list of public methods available from the QSession class.
- GetValueâ"accepts a session key and a variable name and returns a single session value.
- GetRecordâ"accepts a session key and a variable name and returns a 2D array of the session data.
- GetRecordsâ"accepts a session key and returns a 2D array of all session variables and values for a session.
- Existsâ"determines if a session or a session variable exists already.
The second class is the TSession class, which is responsible for making all of the session changes. This class contains all of my action queries and therefore has a T prefix. I could mark this class as having a COM+ transaction mode of "Requires Transactions", so it always participates in a COM+ transaction. But this would be overkill, and I don't need to incur the overhead of the transaction in this case. Instead I'll mark the class as having "No Transactions". That way the component still runs under COM+ and takes advantage of object pooling, but does not incur transactional overhead. The following is a list of the public methods available from the TSession class.
- CreateSessionâ"creates a session key and returns it in its encrypted form.
- UpdateSessionâ"updates a session value for a given session key and variable.
- DeleteSessionâ"deletes a given session key record.
- UpdateLastAccessDateTimeâ"updates the last access date/time for a given session key.
Initiating a Session Figure 4
shows a code excerpt from the CreateSession method of the TSession class. This method calls the prAPP_Insert_Session stored procedure, which creates a session key and inserts it into the database. Then, the CreateSession method encrypts the new session key and returns it. Why encrypt it? Just to make it more difficult for someone to think they can intercept and use it. I use a basic encryption algorithm (shown in Figure 5
), but any old algorithm will do. (And if you want to skip this feature, the Session Manager will still work.) The session key that is returned from the CreateSession method is stored on the user's machine within a cookie. I'll show you how this works when I get to the ASP portion of the code in a moment.
Retrieving Session Data Figure 6
shows the GetRecords method of the QSession class. First, it checks to make sure there is a session key using the HasValue function (implemented in common.bas). Then, I decrypt the session key and pass it to the GetSelectedRecords private function (shown in Figure 7
), which does all of the dirty work for me.
The GetSelectedRecords function retrieves all of the session data for the given session key. It does this by calling the prAPP_Get_SessionValues stored procedure. This code simply commits the transaction to the database and releases the object from the calling application. This is important since the component runs under COM+.
There is a lot more code in this component that I haven't discussed, but if you want to see the entire source code just download it from the link at the top of this article and give it a go yourself.
Using the Session Manager
Let's assume the Blue_SessionManager has been compiled into an ActiveXÂ® DLL and runs under a COM+ package. Now it is ready to use from the ASP code. Figure 8
shows a standard logon page that I'll use to demonstrate this.Figure 8 Standard Logon Page
Let's assume you type in "Haley" as your user ID and "elmo" as your password. When you click the Logon button, a session is created using a GUID, and entries for the user ID and password (and their respective values) are inserted into the database. A simple Web page like the one shown in Figure 9
appears. It displays the two session variables and values for the session just initiated.Figure 9 Session Values in a Web Page
Simplifying the ASP Calls
To understand the Logon.asp page, you need to review the ASP code that creates the session, inserts the session variables and values, and writes the session key to the user's machine in a cookie. Figure 10
highlights the main ASP function, Connect, in the Logon.asp page. Once it verifies the user, it calls a CreateSession ASP function (which you can see implemented in SessionManager.vbs, downloadable from the link at the top of this article.
The ASP functions in the SessionManager.vbs file should be included in all of the ASP pages. You can use these ASP functions to interface with the Blue_SessionManager component. As you can see from Figure 10
, these ASP functions in the SessionManager.vbs file really simplify the management of the session data.
Calling the ASP function, CreateSession, in turn calls the COM component, which calls the stored procedure to insert the session. Then the new session key bubbles back to the originating ASP page. And all I had to do was call CreateSession!
To make using the Session Manager from ASP as easy as using the ASP Session object, I created some generic functions that mask the complexity of the code. All you really need to know once you have your DLL in place is how to create a session (with the ASP function CreateSession); delete a session (with the ASP function DeleteSession); set a session value (with the ASP function UpdateSession); get a session value (with the ASP function GetAllSessionValues); and check if a session exists (with the ASP function SessionExists). The functions in the SessionManager.vbs file handle this for you.
Then, as you saw in Figure 10
, the UpdateSession ASP function inserts the sUserID session variable for this new session with a value of "Haley." This process repeats for each session variable/value pair. There is no limit other than what is reasonable to maintain. Normally, you should keep the session data short and sweet, only using it for user ID and password or for storing a shopping cart for the user, for example. Finally, the WriteSessionKey ASP function is called (from the Logon Page in Figure 10
At this point, the GetAllSessionValues function gets the session values out. Once this call is made in the SomePage.asp file (see Figure 11
), the session data can be displayed or used for whatever purposes are required. GetAllSessionValues reads the session key from the user's cookie and then calls the Blue_SessionManager component to get all of the session data for this user. The data is returned in a two-dimensional array, which can then be used to access the individual session values that you want. In this case, the code gets the sUserID and sPassword values and sets them to an ASP variable.
You'll probably want to update the last access date/time from each ASP page. You could easily do this by calling the ASP function SessionExists at the beginning of each ASP page. This function returns true or false depending on whether the session is in the database. Why wouldn't a current session be in the database? Well, consider this scenario: say you had a job running SQL Server that deleted the sessions that were unused for 20 minutes or more, and 30 minutes have elapsed since the user last visited the site. The SessionExists function serves the dual purpose of letting you know if the session is still around, and updating the last access date/time, thus keeping the session alive for a little while longer.
You can also use the Session Manager tool to store an entire XML string in a session value. That opens the door to a whole new world of possibilities, as long as you don't mind storing and passing a lot of data to and from the database.
Or you can maintain the session data in memory using something like the COM+ Shared Property Manager (which under Windows 2000 is part of the COM Services component). You could store your state in the Shared Property Manager and have a COM object that communicates with the different Web servers, keeping each Web Server's Shared Property Manager data replicated throughout the Web farm. This challenging approach offers an alternative to using a database as your state repository.
You could also package the data into an object and use Object Pooling. This may result in faster performance. For applications that need to be prepared for a Web farm, the Session Manager could be just the trick. It's ready for you to use to keep your users happy. After all, do you really ever get tired of all that hero worship? For related articles see:Maintaining Session State on your Web FarmFor background information see:Professional ADO 2.5 with ASP 3.0
(Wrox Press) Johnny Papa develops applications for Lancelot Web Solutions, an e-business software solutions company. He is the author of several books, including Professional ADO 2.5 RDS with ASP 3.0 (Wrox, 2000) and Microsoft SQL Server 7 Programming Unleashed (Sams, 1998). As a certified MCT, Johnny has trained hundreds of developers. You can reach him at email@example.com.
From the October 2000 issue of MSDN Magazine.